| name | outfitter-mcp |
| version | 0.1.0 |
| description | Deep patterns for @outfitter/mcp including tool registration, Zod schemas, resources, and server configuration. Use when building MCP servers, registering tools, defining resources, or when "MCP server", "MCP tool", "registerTool", or "@outfitter/mcp" are mentioned. |
| allowed-tools | Read Write Edit Glob Grep |
MCP Server Patterns
Deep dive into @outfitter/mcp patterns.
Creating a Server
import { createMcpServer } from "@outfitter/mcp";
const server = createMcpServer({
name: "my-server",
version: "0.1.0",
description: "Server for AI agents",
});
server.registerTool(searchTool);
server.registerTool(createTool);
server.start();
Tool Definition
Basic Tool
import { Result, ValidationError } from "@outfitter/contracts";
import { z } from "zod";
const InputSchema = z.object({
query: z.string().min(1).describe("Search query"),
limit: z.number().int().positive().default(10).describe("Max results"),
});
export const searchTool = {
name: "search",
description: "Search for items. Use when user asks to find or search.",
inputSchema: InputSchema,
handler: async (
input: z.infer<typeof InputSchema>
): Promise<Result<SearchOutput, ValidationError>> => {
const results = await performSearch(input.query, input.limit);
return Result.ok({ results, total: results.length });
},
};
Schema Best Practices
const InputSchema = z.object({
query: z.string().describe("The search term to look for"),
limit: z.number().default(10).describe("Maximum number of results"),
sortBy: z
.enum(["name", "date", "relevance"])
.default("relevance")
.describe("Field to sort results by"),
tags: z.array(z.string()).optional().describe("Filter by tags"),
});
Tool with Context
export const myTool = {
name: "my_tool",
description: "Tool description",
inputSchema: InputSchema,
handler: async (input, ctx) => {
ctx.logger.debug("Tool invoked", { input });
const result = await myHandler(input, ctx);
if (result.isErr()) {
ctx.logger.error("Tool failed", { error: result.error });
}
return result;
},
};
Resources
Static Resource
server.registerResource({
uri: "config://settings",
name: "Configuration",
description: "Current server configuration",
mimeType: "application/json",
read: async () => {
return JSON.stringify(config, null, 2);
},
});
Dynamic Resource
server.registerResource({
uri: "data://users/{id}",
name: "User Data",
description: "User information by ID",
mimeType: "application/json",
read: async (uri) => {
const id = uri.split("/").pop();
const user = await getUser(id);
return JSON.stringify(user);
},
});
Resource List
server.registerResourceList({
uri: "data://users",
name: "Users",
description: "List of all users",
list: async () => {
const users = await getAllUsers();
return users.map((u) => ({
uri: `data://users/${u.id}`,
name: u.name,
description: u.email,
}));
},
});
Prompts
server.registerPrompt({
name: "analyze",
description: "Analyze data with specific focus",
arguments: [
{ name: "focus", description: "What to focus on", required: true },
{ name: "depth", description: "Analysis depth", required: false },
],
get: async (args) => {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Analyze with focus on: ${args.focus}. Depth: ${args.depth || "normal"}`,
},
},
],
};
},
});
Error Handling
Returning Errors
handler: async (input) => {
if (!input.query) {
return Result.err(ValidationError.create("query", "is required"));
}
const item = await findItem(input.id);
if (!item) {
return Result.err(NotFoundError.create("item", input.id));
}
return Result.ok(item);
};
Error Categories in MCP
| Category | MCP Behavior |
|---|
| validation | Tool returns error with details |
| not_found | Tool returns error with resource info |
| internal | Tool returns generic error, logs full error |
Server Configuration
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "bun",
"args": ["run", "/path/to/server.ts"]
}
}
}
With Environment Variables
{
"mcpServers": {
"my-server": {
"command": "bun",
"args": ["run", "/path/to/server.ts"],
"env": {
"API_KEY": "secret",
"LOG_LEVEL": "debug"
}
}
}
}
Deferred Tool Loading
For tools that are expensive to load:
server.registerDeferredTool({
name: "heavy_tool",
description: "Expensive tool loaded on demand",
load: async () => {
const { heavyTool } = await import("./heavy-tool.js");
return heavyTool;
},
});
Testing MCP Servers
import { createMcpHarness } from "@outfitter/testing";
const harness = createMcpHarness(server);
test("tool returns results", async () => {
const result = await harness.callTool("my-tool", { query: "test" });
expect(result.isOk()).toBe(true);
expect(result.value.results).toHaveLength(3);
});
Best Practices
- Descriptive schemas — Use
.describe() on every field
- Sensible defaults — Provide
.default() where appropriate
- Error categories — Use taxonomy errors for proper handling
- Logging — Log tool invocations for debugging
- Deferred loading — Lazy load expensive tools
- Test harnesses — Use
createMcpHarness for testing
Related Skills
stack:patterns — Handler contract
stack:scaffold — MCP tool template
stack:debug — Troubleshooting MCP issues