npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@outfitter/mcp

v0.5.0

Published

MCP server framework with typed tools for Outfitter

Readme

@outfitter/mcp

MCP (Model Context Protocol) server framework with typed tools and Result-based error handling.

Installation

bun add @outfitter/mcp

Quick Start

import { createMcpServer, defineTool } from "@outfitter/mcp";
import { Result } from "@outfitter/contracts";
import { z } from "zod";

const server = createMcpServer({
  name: "calculator",
  version: "1.0.0",
});

server.registerTool(
  defineTool({
    name: "add",
    description: "Add two numbers together",
    inputSchema: z.object({
      a: z.number(),
      b: z.number(),
    }),
    handler: async (input, ctx) => {
      ctx.logger.debug("Adding numbers", { a: input.a, b: input.b });
      return Result.ok({ sum: input.a + input.b });
    },
  })
);

await server.start();

Features

  • Typed Tools — Define tools with Zod schemas for automatic input validation
  • Result-Based Errors — All operations return Result<T, E> for explicit error handling
  • Handler Contract — Tools use the same Handler pattern as other Outfitter packages
  • Core Tools — Built-in docs, config, and query tools for common patterns
  • Deferred Loading — Support for MCP tool search with deferLoading flag

API Reference

createMcpServer(options)

Creates an MCP server instance.

interface McpServerOptions {
  name: string; // Server name for MCP handshake
  version: string; // Server version (semver)
  logger?: Logger; // Optional structured logger (BYO)
  defaultLogLevel?: McpLogLevel | null; // Default log forwarding level
}

const server = createMcpServer({
  name: "my-server",
  version: "1.0.0",
});

// If `logger` is omitted, Outfitter logger factory defaults are used.

Bring Your Own Logger (BYO)

createMcpServer accepts any logger implementing the shared Logger contract. This lets you use the default Outfitter backend or a custom backend adapter.

Outfitter factory backend

import { createOutfitterLoggerFactory } from "@outfitter/logging";

const loggerFactory = createOutfitterLoggerFactory();
const server = createMcpServer({
  name: "my-server",
  version: "1.0.0",
  logger: loggerFactory.createLogger({
    name: "mcp",
    context: { surface: "mcp" },
  }),
});

Custom adapter backend

import {
  createLoggerFactory,
  type Logger,
  type LoggerAdapter,
} from "@outfitter/contracts";

type BackendOptions = { write: (line: string) => void };

const adapter: LoggerAdapter<BackendOptions> = {
  createLogger(config) {
    const write = config.backend?.write ?? (() => {});
    const createMethod = (level: string): Logger["info"] =>
      ((message: string) => {
        write(`[${level}] ${config.name}: ${message}`);
      }) as Logger["info"];

    return {
      trace: createMethod("trace"),
      debug: createMethod("debug"),
      info: createMethod("info"),
      warn: createMethod("warn"),
      error: createMethod("error"),
      fatal: createMethod("fatal"),
      child: (childContext) =>
        adapter.createLogger({
          ...config,
          context: { ...(config.context ?? {}), ...childContext },
        }),
    };
  },
};

const loggerFactory = createLoggerFactory(adapter);
const server = createMcpServer({
  name: "my-server",
  version: "1.0.0",
  logger: loggerFactory.createLogger({
    name: "mcp",
    backend: { write: (line) => console.log(line) },
  }),
});

Log Forwarding

MCP servers can forward log messages to the connected client. The default log level is resolved from environment configuration:

Precedence (highest wins):

  1. OUTFITTER_LOG_LEVEL environment variable
  2. options.defaultLogLevel
  3. OUTFITTER_ENV profile defaults ("debug" in development, null otherwise)
  4. null (no forwarding)
const server = createMcpServer({
  name: "my-server",
  version: "1.0.0",
  // Forwarding level auto-resolved from OUTFITTER_ENV
});

// With OUTFITTER_ENV=development → forwards at "debug"
// With OUTFITTER_ENV=production → no forwarding (null)
// With OUTFITTER_LOG_LEVEL=error → forwards at "error"

Set defaultLogLevel: null to explicitly disable forwarding regardless of environment. The MCP client can always override via logging/setLevel.

sendLogMessage(level, data, loggerName?)

Send a log message to the connected MCP client.

server.sendLogMessage("info", "Indexing complete", "my-server");
server.sendLogMessage("warning", { message: "Rate limited", retryAfter: 30 });

Only sends if the message level meets or exceeds the current client log level threshold.

defineTool(definition)

Helper for defining typed tools with better type inference.

interface ToolDefinition<TInput, TOutput, TError> {
  name: string; // Unique tool name (kebab-case)
  description: string; // Human-readable description
  inputSchema: z.ZodType<TInput>; // Zod schema for validation
  handler: Handler<TInput, TOutput, TError>;
  deferLoading?: boolean; // Default: true
}

const getUserTool = defineTool({
  name: "get-user",
  description: "Retrieve a user by their unique ID",
  inputSchema: z.object({ userId: z.string().uuid() }),
  handler: async (input, ctx) => {
    const user = await db.users.find(input.userId);
    if (!user) {
      return Result.err(NotFoundError.create("user", input.userId));
    }
    return Result.ok(user);
  },
});

defineResource(definition)

Helper for defining MCP resources.

interface ResourceDefinition {
  uri: string; // Unique resource URI
  name: string; // Human-readable name
  description?: string; // Optional description
  mimeType?: string; // Content MIME type
  handler?: ResourceReadHandler; // Optional resources/read handler
}

const configResource = defineResource({
  uri: "file:///etc/app/config.json",
  name: "Application Config",
  description: "Main configuration file",
  mimeType: "application/json",
  handler: async (uri, ctx) => {
    ctx.logger.debug("Reading config resource", { uri });
    return Result.ok([
      {
        uri,
        mimeType: "application/json",
        text: JSON.stringify({ debug: true }),
      },
    ]);
  },
});

Registered resources with handlers are exposed through MCP resources/read.

server.registerResource(configResource);

const contentResult = await server.readResource("file:///etc/app/config.json");

defineResourceTemplate(definition)

Helper for defining MCP resource templates with URI pattern matching and optional Zod validation.

import { defineResourceTemplate } from "@outfitter/mcp";
import { z } from "zod";

// Basic template with string variables
const userTemplate = defineResourceTemplate({
  uriTemplate: "db:///users/{userId}/profile",
  name: "User Profile",
  handler: async (uri, variables, ctx) => {
    const profile = await getProfile(variables.userId);
    return Result.ok([
      { uri, text: JSON.stringify(profile), mimeType: "application/json" },
    ]);
  },
});

// Typed template with Zod validation and coercion
const postTemplate = defineResourceTemplate({
  uriTemplate: "db:///users/{userId}/posts/{postId}",
  name: "User Post",
  paramSchema: z.object({
    userId: z.string().min(1),
    postId: z.coerce.number().int().positive(),
  }),
  handler: async (uri, params, ctx) => {
    // params is { userId: string; postId: number } — validated and coerced
    const post = await getPost(params.userId, params.postId);
    return Result.ok([{ uri, text: JSON.stringify(post) }]);
  },
});

server.registerResourceTemplate(userTemplate);
server.registerResourceTemplate(postTemplate);

Templates use RFC 6570 Level 1 URI templates ({param} placeholders). Typed templates validate extracted variables against a Zod schema before handler invocation.

Server Methods

interface McpServer {
  readonly name: string;
  readonly version: string;

  // Registration
  registerTool<TInput, TOutput, TError>(tool: ToolDefinition): void;
  registerResource(resource: ResourceDefinition): void;
  registerResourceTemplate(template: ResourceTemplateDefinition): void;

  // Introspection
  getTools(): SerializedTool[];
  getResources(): ResourceDefinition[];
  getResourceTemplates(): ResourceTemplateDefinition[];

  // Invocation
  readResource(uri: string): Promise<Result<ResourceContent[], McpError>>;
  invokeTool<T>(
    name: string,
    input: unknown,
    options?: InvokeToolOptions
  ): Promise<Result<T, McpError>>;

  // Lifecycle
  start(): Promise<void>;
  stop(): Promise<void>;
}

McpHandlerContext

Extended handler context for MCP tools with additional metadata:

interface McpHandlerContext extends HandlerContext {
  toolName?: string; // Name of the tool being invoked
}

Streaming and Progress (@outfitter/mcp/progress)

When an MCP client provides a progressToken in the tool call params, the server automatically creates a progress callback and injects it into the handler context as ctx.progress. Each call emits a notifications/progress notification.

const tool = defineTool({
  name: "index-files",
  description: "Index workspace files",
  inputSchema: z.object({ path: z.string() }),
  handler: async (input, ctx) => {
    const files = await listFiles(input.path);

    for (let i = 0; i < files.length; i++) {
      await indexFile(files[i]);
      ctx.progress?.({ type: "progress", current: i + 1, total: files.length });
    }

    ctx.progress?.({
      type: "step",
      name: "complete",
      status: "complete",
      duration_ms: 150,
    });

    return Result.ok({ indexed: files.length });
  },
});

Use optional chaining (ctx.progress?.()) so the handler works whether or not the client requested progress tracking. Without a progressToken, ctx.progress is undefined and no notifications are sent.

Event mapping to MCP notifications:

| StreamEvent type | MCP progress | MCP total | MCP message | | ---------------- | -------------- | ----------- | ------------------------- | | start | 0 | -- | [start] {command} | | step | 0 | -- | [step] {name}: {status} | | progress | current | total | message (if provided) |

The progress callback uses the same StreamEvent types from @outfitter/contracts/stream as the CLI NDJSON adapter, keeping handlers transport-agnostic.

Programmatic usage (for custom transport layers):

import { createMcpProgressCallback } from "@outfitter/mcp/progress";

const progress = createMcpProgressCallback("tok-123", (notification) =>
  sdkServer.notification(notification)
);

Core Tools

Pre-built tools for common MCP patterns. These are marked with deferLoading: false for immediate availability.

Docs Tool

Provides documentation, usage patterns, and examples.

import { defineDocsTool, createCoreTools } from "@outfitter/mcp";

const docsTool = defineDocsTool({
  docs: {
    overview: "Calculator server for arithmetic operations",
    tools: [{ name: "add", summary: "Add two numbers" }],
    examples: [{ input: { a: 2, b: 3 }, description: "Basic addition" }],
  },
});

// Or use getDocs for dynamic content
const dynamicDocsTool = defineDocsTool({
  getDocs: async (section) => {
    return loadDocsFromFile(section);
  },
});

Config Tool

Read and modify server configuration.

import { defineConfigTool } from "@outfitter/mcp";

const configTool = defineConfigTool({
  initial: { debug: false, maxRetries: 3 },
});

// With custom store
const persistedConfigTool = defineConfigTool({
  store: {
    get: async (key) => db.config.get(key),
    set: async (key, value) => db.config.set(key, value),
    list: async () => db.config.all(),
  },
});

Query Tool

Search and discovery with pagination.

import { defineQueryTool } from "@outfitter/mcp";

const queryTool = defineQueryTool({
  handler: async (input, ctx) => {
    const results = await searchIndex(input.q, {
      limit: input.limit,
      cursor: input.cursor,
      filters: input.filters,
    });
    return Result.ok({
      results: results.items,
      nextCursor: results.nextCursor,
    });
  },
});

Bundle All Core Tools

import { createCoreTools } from "@outfitter/mcp";

const coreTools = createCoreTools({
  docs: { docs: myDocs },
  config: { initial: myConfig },
  query: { handler: myQueryHandler },
});

for (const tool of coreTools) {
  server.registerTool(tool);
}

Tool Annotations

Use TOOL_ANNOTATIONS presets to declare tool behavior hints without manually specifying all four booleans:

import { defineTool, TOOL_ANNOTATIONS } from "@outfitter/mcp";

// Use a preset directly
const listTool = defineTool({
  name: "list-items",
  description: "List all items",
  inputSchema: z.object({}),
  annotations: TOOL_ANNOTATIONS.readOnly,
  handler: async (input, ctx) => {
    /* ... */
  },
});

// Spread and override for edge cases
const searchTool = defineTool({
  name: "search",
  description: "Search external APIs",
  inputSchema: z.object({ q: z.string() }),
  annotations: { ...TOOL_ANNOTATIONS.readOnly, openWorldHint: true },
  handler: async (input, ctx) => {
    /* ... */
  },
});

| Preset | readOnly | destructive | idempotent | openWorld | | ----------------- | -------- | ----------- | ---------- | --------- | | readOnly | true | false | true | false | | write | false | false | false | false | | writeIdempotent | false | false | true | false | | destructive | false | true | true | false | | openWorld | false | false | false | true |

For multi-action tools, use the most conservative union of hints. Per-action annotations are an MCP spec limitation.

adaptHandler

When your handler returns domain errors that extend Error but not OutfitterError, use adaptHandler instead of an unsafe cast:

import { adaptHandler, defineTool } from "@outfitter/mcp";

const tool = defineTool({
  name: "my-tool",
  inputSchema: z.object({ id: z.string() }),
  handler: adaptHandler(myDomainHandler),
});

Transport Helpers

wrapToolResult / wrapToolError

Format handler output as MCP tool responses. Useful when building custom transport layers or testing:

import { wrapToolResult, wrapToolError } from "@outfitter/mcp";

// Wrap a plain value as MCP tool content
const response = wrapToolResult({ count: 42 });
// { content: [{ type: "text", text: '{"count":42}' }] }

// Wrap an error with isError flag
const errorResponse = wrapToolError(new Error("not found"));
// { content: [{ type: "text", text: "not found" }], isError: true }

connectStdio

Connect server to stdio transport for Claude Desktop integration.

import { createMcpServer, connectStdio } from "@outfitter/mcp";

const server = createMcpServer({ name: "my-server", version: "1.0.0" });
// ... register tools ...

await connectStdio(server);

createSdkServer

Create the underlying @modelcontextprotocol/sdk server.

import { createSdkServer } from "@outfitter/mcp";

const { server: sdkServer, toolsList, callTool } = createSdkServer(mcpServer);

Error Handling

Tools return Results with typed errors. The framework automatically translates OutfitterError categories to JSON-RPC error codes:

| Category | JSON-RPC Code | Description | | ------------ | ------------- | ---------------- | | validation | -32602 | Invalid params | | not_found | -32601 | Method not found | | permission | -32600 | Invalid request | | internal | -32603 | Internal error |

const result = await server.invokeTool("get-user", { userId: "123" });

if (result.isErr()) {
  // result.error is McpError with code and context
  console.error(result.error.message, result.error.code);
}

Schema Utilities

zodToJsonSchema

Convert Zod schemas to JSON Schema for MCP protocol.

import { zodToJsonSchema } from "@outfitter/mcp";

const schema = z.object({
  name: z.string(),
  age: z.number().optional(),
});

const jsonSchema = zodToJsonSchema(schema);
// { type: "object", properties: { name: { type: "string" }, ... } }

Action Adapter

buildMcpTools

Build MCP tools from an action registry (for structured action-based servers).

import { buildMcpTools } from "@outfitter/mcp";

const tools = buildMcpTools({
  actions: myActionRegistry,
  prefix: "myapp",
});

for (const tool of tools) {
  server.registerTool(tool);
}

Claude Desktop Configuration

Add your MCP server to Claude Desktop:

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

Config location:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/claude/claude_desktop_config.json

Upgrading

Run outfitter upgrade --guide for version-specific migration instructions, or check the migration docs for detailed upgrade steps.

Related Packages