AgentSkillsCN

outfitter-mcp

深入探讨 @outfitter/mcp 的各项模式,包括工具注册、Zod 架构、资源以及服务器配置。适用于构建 MCP 服务器、注册工具、定义资源,或当提及“MCP 服务器”、“MCP 工具”、“registerTool”或“@outfitter/mcp”时使用。

SKILL.md
--- frontmatter
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

typescript
import { createMcpServer } from "@outfitter/mcp";

const server = createMcpServer({
  name: "my-server",
  version: "0.1.0",
  description: "Server for AI agents",
});

// Register tools before start
server.registerTool(searchTool);
server.registerTool(createTool);

// Start server
server.start();

Tool Definition

Basic Tool

typescript
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

typescript
const InputSchema = z.object({
  // Always use .describe() for AI understanding
  query: z.string().describe("The search term to look for"),

  // Provide defaults where sensible
  limit: z.number().default(10).describe("Maximum number of results"),

  // Use enums for fixed choices
  sortBy: z.enum(["name", "date", "relevance"]).default("relevance")
    .describe("Field to sort results by"),

  // Mark optional fields explicitly
  tags: z.array(z.string()).optional().describe("Filter by tags"),
});

Tool with Context

typescript
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

typescript
server.registerResource({
  uri: "config://settings",
  name: "Configuration",
  description: "Current server configuration",
  mimeType: "application/json",

  read: async () => {
    return JSON.stringify(config, null, 2);
  },
});

Dynamic Resource

typescript
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

typescript
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

typescript
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

typescript
handler: async (input) => {
  if (!input.query) {
    return Result.err(new ValidationError("Query is required"));
  }

  const item = await findItem(input.id);
  if (!item) {
    return Result.err(new NotFoundError("item", input.id));
  }

  return Result.ok(item);
}

Error Categories in MCP

CategoryMCP Behavior
validationTool returns error with details
not_foundTool returns error with resource info
internalTool returns generic error, logs full error

Server Configuration

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

json
{
  "mcpServers": {
    "my-server": {
      "command": "bun",
      "args": ["run", "/path/to/server.ts"]
    }
  }
}

With Environment Variables

json
{
  "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:

typescript
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

typescript
import { createMcpHarness } from "@outfitter/testing";

const harness = createMcpHarness(myTool);

test("tool returns results", async () => {
  const result = await harness.invoke({ query: "test" });

  expect(result.isOk()).toBe(true);
  expect(result.value.results).toHaveLength(3);
});

Best Practices

  1. Descriptive schemas — Use .describe() on every field
  2. Sensible defaults — Provide .default() where appropriate
  3. Error categories — Use taxonomy errors for proper handling
  4. Logging — Log tool invocations for debugging
  5. Deferred loading — Lazy load expensive tools
  6. Test harnesses — Use createMcpHarness for testing

Related Skills

  • stack:patterns — Handler contract
  • stack:scaffold — MCP tool template
  • stack:debug — Troubleshooting MCP issues