Guides
Beginner demo lab#
A copy-paste lab for first-time MCP and agent-connector developers. You will create one local MCP server, run a smoke test without any host UI, open it in Inspector, add a connector config, customize the demo data, and compare your result against simple screenshot-style frames.
What you will have at the end
A workingschema_summary MCP tool, a repeatable npm run demo script, an Inspector check, a connector config with a HUD/statusline and action, and a small set of visual checkpoints you can use when writing docs or release notes for your own package.0. The whole path#
The lab is intentionally linear. Finish each checkpoint before moving to the next one; that keeps protocol problems, host problems, and connector problems separate.
| Step | You do | Done when |
|---|---|---|
| Server | Create files and one read-only tool. | npm run demo prints the tool result. |
| Inspector | Open a protocol-aware UI. | Inspector lists and calls schema_summary. |
| Customize | Change the demo data and schema. | Bad table names fail; valid table names return new data. |
| Connector | Add statusline and action surfaces. | Audit and dry-run install show concrete host output. |
1. Create the files#
Start with this folder shape. The server is neutral MCP. The connector config is the agent-connector layer you add after the server works.
acme-db-mcp/
package.json
my-mcp-server.mjs
agent-connector.config.mjs
scripts/
demo-smoke.mjs{
"name": "@acme/acme-db-mcp",
"version": "0.1.0",
"type": "module",
"scripts": {
"demo": "node scripts/demo-smoke.mjs",
"inspect": "npx -y @modelcontextprotocol/inspector node ./my-mcp-server.mjs"
},
"dependencies": {
"@ken-jo/agent-connector": "^0.4.98",
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^3.25.0"
}
}2. Paste the demo MCP server#
This server has one safe read-only tool. The table list is deliberately fake so beginners can edit it without touching a real database.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const tables = {
users: ["id", "email", "created_at", "plan"],
orders: ["id", "user_id", "total_usd", "status"],
invoices: ["id", "customer_id", "due_date", "paid"],
};
const server = new McpServer({
name: "acme-db-demo",
version: "0.1.0",
});
server.registerTool(
"schema_summary",
{
title: "Schema summary",
description:
"Return a short read-only schema summary for the demo database. Use this before writing SQL.",
inputSchema: {
table: z.enum(["users", "orders", "invoices"]).optional(),
includeColumns: z.boolean().default(false),
},
outputSchema: {
table: z.string().optional(),
summary: z.string(),
columns: z.array(z.string()).optional(),
},
},
async ({ table, includeColumns }) => {
const tableNames = Object.keys(tables);
const selected = table ? tables[table] : undefined;
const structuredContent = {
table,
summary: table
? `${table} has ${selected.length} demo columns.`
: `Demo database has ${tableNames.length} tables: ${tableNames.join(", ")}.`,
columns: includeColumns ? selected : undefined,
};
return {
structuredContent,
content: [{ type: "text", text: structuredContent.summary }],
};
},
);
await server.connect(new StdioServerTransport());Beginner checkpoint
You should be able to explain this file in one sentence: the server exposes a tool namedschema_summary that validates a table name and returns text plus structured JSON.3. Add the smoke-test script#
A smoke script is easier than opening a host while you are still learning. It starts your stdio server as a client would, lists tools, calls one tool, prints the result, and closes the connection.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "node",
args: ["./my-mcp-server.mjs"],
});
const client = new Client({
name: "acme-db-demo-smoke",
version: "0.1.0",
});
await client.connect(transport);
const tools = await client.listTools();
console.log("tools:", tools.tools.map((tool) => tool.name).join(", "));
const result = await client.callTool({
name: "schema_summary",
arguments: { table: "users", includeColumns: true },
});
console.log("text:", result.content?.[0]?.text);
console.log("structured:", JSON.stringify(result.structuredContent, null, 2));
await client.close();npm install
npm run demo
npm run inspect
# After the server works:
npx @ken-jo/agent-connector audit --connector ./agent-connector.config.mjs
npx @ken-jo/agent-connector install --connector ./agent-connector.config.mjs --targets claude-code --dry-run
npx @ken-jo/agent-connector action claude-code show-demo-tables --connector acme-db-demo4. Open Inspector and capture the first demo frame#
Inspector is the best first screenshot because it proves the MCP layer works before any host-specific install is involved. Capture the tool list and one successful schema_summary call.
5. Customize one thing on purpose#
The first customization should change data without changing the mental model. Replace the fake table catalog, update the enum, and rerun the same script. This teaches the right habit: schema and implementation move together.
// Change the catalog first.
const tables = {
products: ["id", "sku", "name", "price_usd"],
inventory: ["id", "product_id", "warehouse", "quantity"],
};
// Then update the enum so invalid table names still fail early.
table: z.enum(["products", "inventory"]).optional()| Try | What you learn |
|---|---|
| Rename the tables | Tool descriptions and schemas should match the actual domain. |
| Add one boolean option | Optional arguments should have a clear default and visible result. |
| Call an invalid table | Zod validation should fail before your handler touches app logic. |
6. Add agent-connector surfaces#
After the MCP tool works, add the connector layer. This example keeps it small: one server declaration, one HUD/statusline string, and one user-invoked action. Unsupported hosts skip-warn; supported hosts emit native affordances.
import {
defineAction,
defineConnector,
defineStatusline,
} from "@ken-jo/agent-connector/sdk";
import { fileURLToPath } from "node:url";
const serverPath = fileURLToPath(new URL("./my-mcp-server.mjs", import.meta.url));
const statusline = defineStatusline({
description: "Show the demo connector state.",
options: { refreshInterval: 5, maxLines: 1 },
render(ctx) {
const calls = ctx.usage?.calls ?? 0;
const host = ctx.host === "unknown" ? "host" : ctx.host;
return `acme-db demo · ${host} · ${calls} calls`;
},
});
const showTables = defineAction({
id: "show-demo-tables",
label: "Show demo tables",
description: "Print the demo table list.",
placement: "command-palette",
run(ctx) {
return {
message: `Demo tables for ${ctx.host}: users, orders, invoices`,
};
},
});
export default defineConnector({
displayName: "Acme DB Demo",
server: {
transport: "stdio",
command: "node",
args: [serverPath],
},
statusline,
actions: [showTables],
});You have an MCP tool named schema_summary. Please inspect the users table, explain what columns exist, and suggest one safe read-only query a beginner could try next.
7. Capture docs-ready demo screenshots#
For a beginner-facing README or release note, capture only the screens that prove a boundary. More screenshots are not better; they become noise unless each one answers a different question.
| Screenshot | Proves | Keep visible |
|---|---|---|
| Terminal smoke test | The server can be called without a host UI. | Tool name, text result, structured JSON. |
| Inspector call | The MCP protocol layer works. | Arguments and successful result, not unrelated browser chrome. |
| Host chat answer | A real host can use the tool. | The prompt, the host's answer, and the tool result summary. |
| HUD/action preview | agent-connector surfaces are wired. | Short HUD string and one explicit action result. |
Next pages
Continue with Build your first MCP server for the protocol walkthrough, then Connect your first host and Add connector surfaces.