Skip to main content

MCP Tools

Kickstart ships an MCP server (packages/mcp-server/) that integrates with IDE-based AI assistants — VS Code Copilot, Claude Code, and any other client that speaks the Model Context Protocol. The server exposes Kickstart's conversation engine, manifest generation, and A2UI responses as MCP tools over stdio transport.

This guide covers the MCP server architecture, tool registration pattern, A2UI capability negotiation, and how to add a new tool.

How MCP Tools Work​

Server Setup​

The entry point is packages/mcp-server/src/index.ts. It creates an McpServer instance, registers tools and resources, and connects via StdioServerTransport:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
name: "kickstart",
version: "0.1.0",
});

// Register tools here...

const transport = new StdioServerTransport();
await server.connect(transport);

Tool Registration Pattern​

Tools are registered using server.tool() with Zod schemas for parameter validation:

import { z } from "zod";

server.tool(
"tool-name",
"Human-readable description for the LLM",
{
paramName: z.string().describe("Parameter description"),
optionalParam: z.number().optional().describe("Optional parameter"),
},
async (params) => handleToolName(sessions, params.paramName),
);
ArgumentDescription
Name (string)Unique tool identifier — this is what the IDE's LLM calls
Description (string)Sent to the LLM to explain when to use the tool
Schema (object)Zod schema — auto-validated before the handler runs
Handler (function)Async function receiving validated params, returns { content: ContentItem[] }

Existing Tools​

ToolDescription
kickstartStart a new Kickstart conversation. Returns an A2UI phase indicator and welcome text.
converseContinue a multi-turn conversation. Processes user message through the phase machine.
generate-manifestsGenerate Kubernetes manifests and GitHub Actions workflows from conversation state.
check-statusCheck deployment status for an active session.
actionHandle a user action from the A2UI interface (button click, form submission).
app-messageRelay a message from the MCP App HTML surface through the appropriate handler.

Additionally, a resource is registered:

ResourceURIDescription
kickstart-appkickstart://app/mainHTML surface for IDE-native conversation UI (MCP App)

Handler Pattern​

Tool handlers live in packages/mcp-server/src/tools/ and follow a consistent pattern:

type ContentItem =
| { type: "text"; text: string }
| { type: "resource"; resource: { uri: string; mimeType: string; text: string } };

export async function handleMyTool(
sessions: Map<string, SessionState>,
param: string,
capability: A2UICapability = "kickstart",
): Promise<{ content: ContentItem[] }> {
// 1. Validate session (if session-scoped)
const session = sessions.get(param);
if (!session) {
return {
content: [{ type: "text", text: "❌ Session not found." }],
};
}

// 2. Perform logic using @kickstart/core
const result = await doSomething(session);

// 3. Build A2UI component (optional)
const component = { type: "Card", id: "my-card", title: "Result", children: [] };

// 4. Wrap with createA2UIResource() for capability degradation
const a2uiResource = createA2UIResource(
component,
`a2ui://kickstart/session/${session.sessionId}/my-tool`,
capability,
);

// 5. Return content items
const content: ContentItem[] = [{ type: "text", text: result.summary }];
if (a2uiResource) content.push(a2uiResource);

return { content };
}

A2UI Capability Tiers​

Not all MCP clients support A2UI. The server negotiates capabilities during the initialize handshake and degrades gracefully:

TierConditionBehavior
kickstartClient advertises the Kickstart catalog IDFull custom A2UI components
basicClient advertises any A2UI catalogComponents degraded to Card + Text
noneNo A2UI supportText-only responses (A2UI resources omitted)

Capability resolution is handled by resolveA2UICapability() in packages/mcp-server/src/a2ui.ts:

export function resolveA2UICapability(
clientCatalogs: readonly string[] | undefined,
): A2UICapability {
if (!clientCatalogs || clientCatalogs.length === 0) return "none";
if (clientCatalogs.includes(KICKSTART_CATALOG_ID)) return "kickstart";
if (clientCatalogs.length > 0) return "basic";
return "none";
}

Always wrap A2UI components with createA2UIResource() — it handles degradation automatically and returns null when the client has no A2UI support:

const resource = createA2UIResource(component, uri, capability);
if (resource) content.push(resource); // Only added when client supports A2UI

Session Management​

The MCP server uses an in-memory Map<string, SessionState> for session storage:

  • TTL: 1 hour per session
  • Cleanup: Stale sessions are purged every 10 minutes
  • Engine state: Stored separately in a module-level Map in packages/mcp-server/src/tools/kickstart.ts

Sessions are created by the kickstart tool and referenced by sessionId in all subsequent tool calls.


How to Add an MCP Tool​

Step 1 — Create the handler file​

Create a new file in packages/mcp-server/src/tools/:

packages/mcp-server/src/tools/my-tool.ts

Implement the handler following the standard pattern:

import type { SessionState } from "@kickstart/core";
import { createA2UIResource } from "../a2ui.js";
import type { A2UICapability } from "../a2ui.js";

type ContentItem =
| { type: "text"; text: string }
| { type: "resource"; resource: { uri: string; mimeType: string; text: string } };

export async function handleMyTool(
sessions: Map<string, SessionState>,
sessionId: string,
capability: A2UICapability = "kickstart",
): Promise<{ content: ContentItem[] }> {
const session = sessions.get(sessionId);
if (!session) {
return {
content: [{
type: "text",
text: `❌ Session \`${sessionId}\` not found. Start a new conversation with the \`kickstart\` tool.`,
}],
};
}

// Your logic here — use @kickstart/core APIs
const summary = `Processed session ${sessionId}`;

return {
content: [{ type: "text", text: summary }],
};
}

Step 2 — Register in index.ts​

Open packages/mcp-server/src/index.ts and add the import and tool registration:

import { handleMyTool } from "./tools/my-tool.js";

// ... after existing tool registrations ...

server.tool(
"my-tool",
"Description of what this tool does — the IDE LLM uses this to decide when to call it.",
{
sessionId: z.string().describe("Active session ID from a kickstart conversation"),
},
async (params) => handleMyTool(sessions, params.sessionId, clientCapability),
);

:::tip Write clear descriptions The IDE's LLM reads the tool description to decide when to call it. Be specific about what the tool does, what input it needs, and what it returns. If your tool requires a session, say so. :::

Step 3 — Add A2UI responses (optional)​

If your tool should return structured UI, build A2UI components and wrap them with createA2UIResource():

import type { CardComponent, TextComponent } from "@kickstart/core";

const text: TextComponent = {
type: "Text",
id: "my-result-text",
content: "Here are the results...",
};

const card: CardComponent = {
type: "Card",
id: "my-result-card",
title: "My Tool Results",
children: [text],
};

const resource = createA2UIResource(
card,
`a2ui://kickstart/session/${sessionId}/my-tool`,
capability,
);

const content: ContentItem[] = [{ type: "text", text: summary }];
if (resource) content.push(resource);

Step 4 — Write tests​

Add tests in packages/mcp-server/src/__tests__/:

import { handleMyTool } from "../tools/my-tool.js";
import type { SessionState } from "@kickstart/core";
import { InMemoryArtifactStore } from "@kickstart/core";

describe("handleMyTool", () => {
const sessions = new Map<string, SessionState>();

beforeEach(() => {
sessions.set("test-session", {
sessionId: "test-session",
currentPhase: "discover",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
appDefinition: {},
messages: [],
artifactStore: new InMemoryArtifactStore(),
});
});

it("returns results for a valid session", async () => {
const result = await handleMyTool(sessions, "test-session", "none");
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe("text");
});

it("returns error for unknown session", async () => {
const result = await handleMyTool(sessions, "nonexistent", "none");
expect(result.content[0]).toMatchObject({
type: "text",
text: expect.stringContaining("not found"),
});
});
});

Step 5 — Build and test locally​

npm run build -w @kickstart/mcp-server
npm run test -w @kickstart/mcp-server

Step 6 — Configure IDE clients​

To test with an IDE client, add the MCP server to the client's configuration.

VS Code Copilot (.vscode/mcp.json):

{
"servers": {
"kickstart": {
"type": "stdio",
"command": "node",
"args": ["packages/mcp-server/dist/index.js"]
}
}
}

Claude Code (~/.claude/mcp.json):

{
"mcpServers": {
"kickstart": {
"type": "stdio",
"command": "node",
"args": ["packages/mcp-server/dist/index.js"]
}
}
}

After configuring, restart the IDE extension and verify your tool appears in the tool list.


Key Files​

FilePurpose
packages/mcp-server/src/index.tsServer entry point — tool and resource registration
packages/mcp-server/src/a2ui.tscreateA2UIResource(), capability resolution, degradation helpers
packages/mcp-server/src/tools/kickstart.tshandleKickstart() — session creation and engine state management
packages/mcp-server/src/tools/converse.tshandleConverse() — multi-turn conversation processing
packages/mcp-server/src/tools/generate-manifests.tshandleGenerateManifests() — manifest generation with safeguard validation
packages/mcp-server/src/tools/check-status.tshandleCheckStatus() — deployment status check
packages/mcp-server/src/tools/action.tshandleAction() — A2UI action event processing
packages/mcp-server/src/app/protocol.tsMCP App postMessage protocol parsing
packages/mcp-server/package.jsonPackage config — binary entry point at kickstart-mcp