MCP服务器构建工具

一个轻量级的ESM封装工具,用于使用TypeScript和Bun构建MCP服务器,提供提示、工具、资源管理和日志功能。

  • by johnlindquist
  • TypeScript
  • 本地部署
  • 7 星标
  • 更新于
在 GitHub 查看 johnlindquist/mcpez

使用说明

mcpez

一个极简、符合人体工程学的ESM封装工具,用于使用TypeScript和Bun构建MCP服务器。

安装

bun add mcpez

快速入门

最小示例

提示

import { prompt, z } from "mcpez"

prompt(
  "review-code",
  {
    description: "Review code for best practices and potential issues",
    argsSchema: {
      code: z.string(),
    },
  },
  ({ code }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Please review this code:\n\n${code}`,
        },
      },
    ],
  }),
)

注Zod 已与 mcpez 打包在一起,因此您无需单独安装它。

为什么Zod被打包在一起

mcpez捆绑了Zod v3以确保与MCP SDK的兼容性,因为MCP SDK特别要求使用Zod v3。由于Zod v4引入了破坏性变更,可能会导致运行时错误,如 keyValidator._parse is not a function通过捆绑Zod v3,可以避免版本冲突,并为开发者提供更简洁、无错误的开发体验。您可以导入 z 直接来自mcpez:

工具

import { tool, z } from "mcpez"

tool(
  "add",
  {
    description: "Add two numbers",
    inputSchema: {
      a: z.number(),
      b: z.number(),
    },
  },
  async ({ a, b }) => {
    const result = a + b
    return {
      content: [{ type: "text", text: `${a} + ${b} = ${result}` }],
    }
  },
)

// No need to manually call startServer(); the server boots on the next tick.

资源

import { resource } from "mcpez"

type EnvironmentConfig = {
  settings: {
    databaseUrl: string
    featureFlags: Record<string, boolean>
  }
  secrets: {
    apiKey: string
  }
}

// In-memory configuration data keyed by deployment environment.
const environmentConfigs = new Map<string, EnvironmentConfig>([
  [
    "production",
    {
      settings: {
        databaseUrl: "postgresql://prod.db.internal/app",
        featureFlags: {
          betaDashboard: false,
          useV2Search: true,
        },
      },
      secrets: {
        apiKey: "prod-12345",
      },
    },
  ],
  [
    "staging",
    {
      settings: {
        databaseUrl: "postgresql://staging.db.internal/app",
        featureFlags: {
          betaDashboard: true,
          useV2Search: true,
        },
      },
      secrets: {
        apiKey: "staging-67890",
      },
    },
  ],
  [
    "development",
    {
      settings: {
        databaseUrl: "postgresql://localhost:5432/app",
        featureFlags: {
          betaDashboard: true,
          useV2Search: false,
        },
      },
      secrets: {
        apiKey: "dev-abcde",
      },
    },
  ],
])

resource(
  "environment-config",
  "config://environment",
  {
    description: "Environment-specific configuration values with optional secrets.",
    mimeType: "application/json",
  },
  async (uri) => {
    const params = uri.searchParams
    const environment = params.get("env") ?? "production"

    const config = environmentConfigs.get(environment)
    if (!config) {
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(
              {
                error: `Unknown environment: ${environment}`,
                availableEnvironments: Array.from(environmentConfigs.keys()),
              },
              null,
              2,
            ),
          },
        ],
      }
    }

    const includeSecrets = params.get("secrets") === "true"

    const payload = {
      environment,
      settings: config.settings,
      ...(includeSecrets ? { secrets: config.secrets } : {}),
      generatedAt: new Date().toISOString(),
    }

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(payload, null, 2),
        },
      ],
    }
  },
)

资源模板

import { resourceTemplate } from "mcpez"

type LogLevel = "info" | "warning" | "error"
type LogEntry = {
  timestamp: string
  message: string
  context?: Record<string, unknown>
}

// Structured audit logs keyed by ISO date and severity level.
const auditLogStore: Record<string, Record<LogLevel, LogEntry[]>> = {
  "2024-04-01": {
    info: [
      { timestamp: "2024-04-01T08:00:00Z", message: "Deployment pipeline triggered" },
      { timestamp: "2024-04-01T08:05:12Z", message: "Deployment completed successfully" },
    ],
    warning: [
      {
        timestamp: "2024-04-01T09:12:33Z",
        message: "Retrying connection to Redis leader",
        context: { attempts: 2 },
      },
    ],
    error: [
      {
        timestamp: "2024-04-01T09:15:00Z",
        message: "Payment gateway timeout",
        context: { orderId: "ORD-481516" },
      },
    ],
  },
  "2024-04-02": {
    info: [
      { timestamp: "2024-04-02T07:45:00Z", message: "Background sync completed" },
      { timestamp: "2024-04-02T10:30:00Z", message: "New feature flag enabled" },
    ],
    warning: [
      {
        timestamp: "2024-04-02T11:05:48Z",
        message: "Slow database query detected",
        context: { durationMs: 830, query: "SELECT * FROM invoices" },
      },
    ],
    error: [],
  },
}

const strictLevels: LogLevel[] = ["info", "warning", "error"]

const firstValue = (value: string | string[] | undefined): string | undefined =>
  Array.isArray(value) ? value[0] : value

resourceTemplate(
  "audit-log",
  {
    name: "audit-log",
    title: "Audit log by date and level",
    uriTemplate: "audit-log://{date}/{level}",
    description: "Retrieve stored audit log entries filtered by ISO date and severity level.",
  },
  {
    description: "Audit log entries grouped by date and severity level.",
    mimeType: "application/json",
  },
  async (uri, variables) => {
    const typedVariables = variables as Record<string, string | string[] | undefined>
    const date = firstValue(typedVariables.date)
    const level = firstValue(typedVariables.level)

    if (!date || !level) {
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(
              {
                error: "Both {date} and {level} must be provided in the URI template.",
                expectedUri: "audit-log://2024-04-01/error",
              },
              null,
              2,
            ),
          },
        ],
      }
    }

    const normalizedLevel = level.toLowerCase()
    if (!strictLevels.includes(normalizedLevel as LogLevel)) {
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(
              {
                error: `Unsupported log level: ${level}`,
                supportedLevels: strictLevels,
              },
              null,
              2,
            ),
          },
        ],
      }
    }

    const entriesByLevel = auditLogStore[date]
    if (!entriesByLevel) {
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(
              {
                error: `No logs found for ${date}.`,
                availableDates: Object.keys(auditLogStore),
              },
              null,
              2,
            ),
          },
        ],
      }
    }

    const entries = entriesByLevel[normalizedLevel as LogLevel]

    if (!entries || entries.length === 0) {
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(
              {
                message: `No ${normalizedLevel} logs found for ${date}.`,
                availableLevels: strictLevels.filter((item) => entriesByLevel[item].length > 0),
              },
              null,
              2,
            ),
          },
        ],
      }
    }

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(
            {
              date,
              level: normalizedLevel,
              count: entries.length,
              entries,
            },
            null,
            2,
          ),
        },
      ],
    }
  },
)

日志记录和通知

import { getServer, log, notifyToolListChanged, tool } from "mcpez"

// Register a simple tool
tool("greet", { description: "Greet the user" }, async () => {
  // Send a log message to the client
  log.info("Greeting tool was called")

  return {
    content: [
      {
        type: "text",
        text: "Hello from mcpez!",
      },
    ],
  }
})

// Register another tool that modifies the tool list
tool("add_tool", { description: "Simulate adding a new tool" }, async () => {
  log.info("New tool would be added here")

  // Notify the client that the tool list has changed
  notifyToolListChanged()

  return {
    content: [
      {
        type: "text",
        text: "Tool list changed!",
      },
    ],
  }
})

// Example of using getServer() for advanced operations
const server = getServer()
if (server) {
  log.debug("Server is running, can access advanced APIs")
} else {
  log.debug("Server not started yet, logging is queued")
}

完全配置的示例

import { prompt, resource, startServer, tool, z } from "mcpez"

tool(
  "echo",
  {
    description: "Echoes back the provided message",
    inputSchema: { message: z.string() },
  },
  async ({ message }) => {
    const output = { echo: `Tool echo: ${message}` }
    return {
      content: [{ type: "text", text: JSON.stringify(output) }],
    }
  },
)

resource(
  "echo",
  "echo://message",
  {
    description: "Echoes back messages as resources",
  },
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: `Resource echo: Hello!`,
      },
    ],
  }),
)

prompt(
  "echo",
  {
    description: "Creates a prompt to process a message",
    argsSchema: { message: z.string() },
  },
  ({ message }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Please process this message: ${message}`,
        },
      },
    ],
  }),
)

// Start with custom server name and version
await startServer("example-full-server", { version: "1.0.0" })

API

  • prompt(name, options, handler)
  • tool(name, options, handler)
  • resource(name, options)
  • resourceTemplate(name, options)
  • startServer(name, serverOptions?, transport?)
  • getServer() - 获取正在运行的服务器实例
  • log.info(data, logger?) - 发送一条日志消息(其他辅助工具: debug, notice, warning, error, critical, alert, emergency)
  • notifyResourceListChanged() - 资源变更时通知
  • notifyToolListChanged() - 当工具发生变化时通知
  • notifyPromptListChanged() - 当提示发生变化时通知

全部 register* 可以在之前进行通话 startServer它们被排队并依次应用 当服务器启动时。 startServer 是可选的,默认为 StdioServerTransport。

仅限ESM(欧洲稳定机制)

这个包仅发布ESM(ECMAScript模块)格式。请确保您的项目已 "type": "module" 或使用 ES模块导入语法。

许可证

麻省理工学院(MIT)