.cursor/skills/implementing-mcp-tools/SKILL.md
Implement new MCP tools in the deno-mcp-template project. Provides the exact file structure, type signatures, registration steps, and patterns for standard tools, sampling tools, form and URL elicitation, resource-backed tools, and notification tools. Use when adding a new tool, creating MCP tools, or asking how tools work in this project.
npx skillsauth add phughesmcr/deno-mcp-template implementing-mcp-toolsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Task Progress:
- [ ] Step 1: Create tool file in src/mcp/tools/
- [ ] Step 2: Define Zod schema, name, config, and callback
- [ ] Step 3: Export as default ToolModule
- [ ] Step 4: Register in src/mcp/tools/mod.ts
- [ ] Step 5: Run `deno task ci` to verify
Every tool follows this structure. Create a new file in src/mcp/tools/.
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod/v3";
import type { ToolConfig, ToolModule } from "$/shared/types.ts";
import {
createCallToolErrorResponse,
createCallToolTextResponse,
} from "$/shared/utils.ts";
// 1. Input schema
const schema = z.object({
myField: z.string().min(1, "Required").describe("What this field does"),
});
// 2. Tool name (kebab-case)
const name = "my-tool-name";
// 3. Config
// deno-lint-ignore no-explicit-any
const config: ToolConfig<typeof schema.shape, any> = {
title: "Human-readable title",
description: "What this tool does — shown to the LLM",
inputSchema: schema.shape,
};
// 4. Callback factory: receives McpServer, returns the handler
// deno-lint-ignore no-explicit-any
const callback = (_mcp: McpServer) => async (args: any): Promise<CallToolResult> => {
const parsed = schema.safeParse(args);
if (!parsed.success) {
return createCallToolErrorResponse({
error: "Invalid arguments",
details: parsed.error.flatten(),
received: args,
});
}
const { myField } = parsed.data;
// ... tool logic ...
return createCallToolTextResponse({ result: myField });
};
// 5. Export as ToolModule tuple
// deno-lint-ignore no-explicit-any
const module: ToolModule<typeof schema.shape, any> = [name, config, callback];
export default module;
In src/mcp/tools/mod.ts:
import myTool from "./myTool.ts";tools array:export const tools: ToolModule<any>[] = [
// ... existing tools
myTool,
];
The ToolManager in the same file handles binding and registration automatically.
From src/shared/types.ts:
ToolModule<InputArgs, OutputArgs> — Export format: [name, config, callbackFactory]ToolConfig<InputArgs, OutputArgs> — { title, description, inputSchema, outputSchema?, annotations? }ToolPlugin — Bound format: [name, config, callback] (created by ToolManager)From src/shared/utils.ts:
createCallToolTextResponse(obj, structuredContent?) — Wraps obj as JSON text contentcreateCallToolErrorResponse(obj, structuredContent?) — Same but sets isError: trueAlways validate with Zod safeParse. For optional args, use args ?? {}:
const parsed = schema.safeParse(args ?? {});
if (!parsed.success) {
return createCallToolErrorResponse({
error: "Invalid arguments",
details: parsed.error.flatten(),
received: args,
});
}
Wrap operational logic in try/catch:
try {
const result = await doSomething(parsed.data);
return createCallToolTextResponse({ result });
} catch (error) {
return createCallToolErrorResponse({
error: error instanceof Error ? error.message : "Unknown error",
operation: "my-operation",
});
}
Use _mcp (unused) in the callback factory. See the template above.
Use mcp.server.createMessage() to request LLM completions:
const callback = (mcp: McpServer) => async (args: any): Promise<CallToolResult> => {
// ... validate args ...
const response = await mcp.server.createMessage(
{
messages: [{ role: "user", content: { type: "text", text: prompt } }],
maxTokens: 1024,
temperature: 0.7,
},
{ timeout: 30000 },
);
const content = Array.isArray(response.content)
? response.content[0]
: response.content;
if (!content || content.type !== "text") {
return createCallToolErrorResponse({ error: "No text response from sampling" });
}
return createCallToolTextResponse({ result: content.text });
};
Experimental elicitation is enabled in src/mcp/serverDefinition.ts (experimentalElicitation: true). Two modes matter in practice:
| Mode | Use case | Mechanism |
|------|----------|-----------|
| form | Structured, non-sensitive fields; validated in the MCP client UI | await mcp.server.elicitInput({ mode: "form", message, requestedSchema }) |
| url | Sensitive or browser-only flows (confirmations, OAuth, secrets typed in your page) | Throw UrlElicitationRequiredError with a URL; complete out-of-band, then createElicitationCompletionNotifier fires notifications/elicitation/complete |
Use mcp.server.elicitInput() with a JSON Schema (requestedSchema):
const callback = (mcp: McpServer) => async (args: any): Promise<CallToolResult> => {
// ... validate args ...
const result = await mcp.server.elicitInput({
mode: "form",
message: "Please provide details",
requestedSchema: {
type: "object",
properties: {
name: { type: "string", title: "Name", description: "Your name" },
},
required: ["name"],
},
});
return createCallToolTextResponse({ elicitationResult: result });
};
Branch on result.action (accept with content, decline, or cancelled) before assuming data exists. For multiple steps, call elicitInput more than once in one tool (see src/mcp/tools/elicitFormWizard.ts).
The client opens a browser URL you control. The tool does not return a normal success result; it throws UrlElicitationRequiredError from @modelcontextprotocol/sdk/types.js with an array of { mode: "url", message, url, elicitationId }.
In this template:
McpServerFactoryContext includes urlElicitation: { baseUrl, registry } (src/mcp/context.ts). Resolve baseUrl with MCP_PUBLIC_BASE_URL or the derived bind URL (src/shared/publicBaseUrl.ts).mcp.server.createElicitationCompletionNotifier(elicitationId), then ctx.urlElicitation.registry.registerPending({ elicitationId, sessionId, label, completionNotifier }).extra.sessionId on the tool handler (streamable HTTP). STDIO has no session; return createCallToolErrorResponse instead of throwing.src/app/http/urlElicitationRoutes.ts (GET/POST /mcp-elicitation/confirm). They validate session + elicitation id against the registry and active transport, then call registry.complete(elicitationId). Bearer auth is skipped for /mcp-elicitation so users are not prompted for the MCP token in a normal browser tab (src/app/http/httpBearerAuthMiddleware.ts).registerUrlElicitationDemoTool in src/mcp/tools/urlElicitationDemo.ts, wired from src/mcp/mod.ts when mcpServerDefinition.urlElicitationDemo is true.Do not log secrets submitted on elicitation HTML forms.
Import resource helpers and mutate state. Resource subscriptions handle notifications automatically via KV watchers:
import { COUNTER_URI, incrementCounterValue } from "../resources/counter.ts";
// In callback:
const value = await incrementCounterValue(delta);
return createCallToolTextResponse({ value, uri: COUNTER_URI });
Use mcp.server.sendLoggingMessage():
await mcp.server.sendLoggingMessage({
level: "info", // debug | info | notice | warning | error | critical | alert | emergency
logger: "my-logger",
data: { message: "Something happened" },
});
Use mcp.sendToolListChanged(), mcp.sendPromptListChanged(), or mcp.sendResourceListChanged() to notify clients that available items have changed.
Add annotations to config for client hints:
const config: ToolConfig<typeof schema.shape, any> = {
title: "My Tool",
description: "...",
inputSchema: schema.shape,
annotations: {
title: "My Tool",
readOnlyHint: true,
openWorldHint: false,
},
};
tools
Implement new MCP resources and resource templates in the deno-mcp-template project. Provides the exact file structure, type signatures, registration steps, and patterns for static resources, KV-backed resources, resource templates with URI patterns, and subscription-based updates. Use when adding a new resource, creating MCP resources, or asking how resources work in this project.
tools
Implement new MCP prompts in the deno-mcp-template project. Provides the exact file structure, type signatures, registration steps, and patterns for prompts with static arguments or dynamic completions. Use when adding a new prompt, creating MCP prompts, or asking how prompts work in this project.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------