name: implementing-mcp-tools description: 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.
Implementing MCP Tools
Workflow
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
Tool File Template
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;
Registration
In src/mcp/tools/mod.ts:
- Import the tool:
import myTool from "./myTool.ts"; - Add it to the
toolsarray:
export const tools: ToolModule<any>[] = [
// ... existing tools
myTool,
];
The ToolManager in the same file handles binding and registration automatically.
Key Types
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 byToolManager)
Response Helpers
From src/shared/utils.ts:
createCallToolTextResponse(obj, structuredContent?)— Wrapsobjas JSON text contentcreateCallToolErrorResponse(obj, structuredContent?)— Same but setsisError: true
Validation Pattern
Always 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,
});
}
Error Handling Pattern
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",
});
}
Tool Categories
Standard Tool (no MCP server interaction)
Use _mcp (unused) in the callback factory. See the template above.
Sampling Tool (LLM-powered)
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 });
};
Elicitation Tool (user input)
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 |
Form mode
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).
URL mode
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:
McpServerFactoryContextincludesurlElicitation: { baseUrl, registry }(src/mcp/context.ts). ResolvebaseUrlwithMCP_PUBLIC_BASE_URLor the derived bind URL (src/shared/publicBaseUrl.ts).- Register pending state and the SDK notifier:
mcp.server.createElicitationCompletionNotifier(elicitationId), thenctx.urlElicitation.registry.registerPending({ elicitationId, sessionId, label, completionNotifier }). - Require
extra.sessionIdon the tool handler (streamable HTTP). STDIO has no session; returncreateCallToolErrorResponseinstead of throwing. - Browser routes live in
src/app/http/urlElicitationRoutes.ts(GET/POST/mcp-elicitation/confirm). They validate session + elicitation id against the registry and active transport, then callregistry.complete(elicitationId). Bearer auth is skipped for/mcp-elicitationso users are not prompted for the MCP token in a normal browser tab (src/app/http/httpBearerAuthMiddleware.ts). - Reference implementation:
registerUrlElicitationDemoToolinsrc/mcp/tools/urlElicitationDemo.ts, wired fromsrc/mcp/mod.tswhenmcpServerDefinition.urlElicitationDemois true.
Do not log secrets submitted on elicitation HTML forms.
Resource-Backed Tool
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 });
Notification Tool (logging)
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" },
});
Notification Tool (list changed)
Use mcp.sendToolListChanged(), mcp.sendPromptListChanged(), or mcp.sendResourceListChanged() to notify clients that available items have changed.
Annotations
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,
},
};
Additional Resources
- For complete tool examples by category, see examples.md
- MCP Tool spec: https://modelcontextprotocol.io/specification/2025-06-18/server/tools
- MCP Sampling: https://modelcontextprotocol.io/docs/concepts/sampling
- MCP Elicitation: https://modelcontextprotocol.io/docs/concepts/elicitation