• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
编写人生
写写代码,写写人生
博客园    首页    新随笔    联系   管理    订阅  订阅

如何更优雅地提供 MCP Resources

本文使用AI辅助编写。

在设计 MCP Server 时,很多人会先想到 tools。比如连接 SQL Server 后,可以提供 list_tables、describe_table、execute_query、get_procedure_definition 等工具。这样确实可以让 AI 查询数据库结构,也可以辅助分析 SQL 问题。但如果只使用 tools,会失去 MCP Resources 的一个重要价值:资源可以成为 Agent 客户端中的一等上下文对象。

现代 AI 编程工具,例如 Claude Code、VS Code Copilot Chat 等,在用户体验上已经不只是简单地展示“AI 调用了某个工具”。当 AI 读取一个文件时,客户端通常会显示“正在读取 file1”,用户点击这个文件名还能跳转到对应文件。这种体验背后有一个关键前提:被读取的对象不是一段普通文本,而是一个可识别、可定位、可展示的资源对象。对于数据库 MCP 来说,表、视图、存储过程、索引、触发器等对象,也应该尽量被设计成类似“文件”的资源,而不是隐藏在普通工具返回值里。

Resource 与 Tool 的边界

MCP 中的 Resource 更适合表达“已经存在、可以被引用、可以被读取的上下文对象”。例如:

mssql://local/db/sales/schema/dbo/table/Orders
mssql://local/db/sales/schema/dbo/procedure/GetOrders
mssql://local/db/sales/schema/dbo/view/OrderSummary
mssql://local/db/sales/schema/dbo/table/Orders/indexes

这些 URI 表示的是数据库中的稳定对象。用户可以通过 @ 引用它们,Agent 客户端也可以通过资源浏览、搜索、补全等方式把它们加入上下文。相比之下,Tool 更适合表达“动作”。例如搜索对象、解析依赖、执行只读查询、诊断 SQL 错误、分析执行计划等,都更适合作为工具。

因此,一个 SQL Server MCP 不应该只暴露 tools,也不应该把数据库中所有对象一股脑注册为 resources。更合理的设计是:用 resources 表达数据库对象,用 resource templates 表达可参数化的资源路径,用 tools 帮助模型发现、解析和读取相关资源。

代码层面可以先把资源注册集中起来,避免所有逻辑散落在 server 初始化代码中:

// resources/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { registerSqlEntryResources } from "./entries.js";
import { registerSqlResourceTemplates } from "./templates.js";
import { registerSqlResourceTools } from "./tools.js";

export function registerResources(server: McpServer) {
  registerSqlEntryResources(server);
  registerSqlResourceTemplates(server);
  registerSqlResourceTools(server);
}

这个结构的重点不是文件怎么命名,而是把三类事情分清楚:入口资源、动态资源模板、模型可调用的资源辅助工具。

不要把所有数据库对象平铺到 resources/list

SQL Server 中的对象数量可能非常多。一个实例下面可能有多个 database,每个 database 下有多个 schema,每个 schema 下又有大量 table、view、procedure、function、index。如果 resources/list 一次性返回所有对象,不仅性能差,也会让客户端资源列表变得不可用。

更优雅的做法是让 resources/list 只返回少量入口资源或高价值资源。例如:

mssql://local
mssql://local/db/sales
mssql://local/db/sales/schemas
mssql://local/db/sales/schema/dbo
mssql://local/db/sales/schema/dbo/tables
mssql://local/db/sales/schema/dbo/views
mssql://local/db/sales/schema/dbo/procedures

这些资源更像目录入口。用户或客户端读取 mssql://local/db/sales/schema/dbo/tables 时,Server 可以返回该 schema 下的表清单;读取具体表 URI 时,Server 再返回表结构、列、主键、外键、索引摘要、行数估计等内容。

入口资源可以这样写:

// resources/entries.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerSqlEntryResources(server: McpServer) {
  server.registerResource(
    "SQL Server Local",
    "mssql://local",
    {
      title: "SQL Server Local",
      description: "Root resource for the local SQL Server instance.",
      mimeType: "text/markdown",
    },
    async (uri) => ({
      contents: [
        {
          uri: uri.toString(),
          mimeType: "text/markdown",
          text: [
            "# SQL Server: local",
            "",
            "Available databases:",
            "",
            "- sales",
            "- hr",
            "- finance",
            "",
            "Use resource templates to access schemas, tables, views, and procedures.",
          ].join("\n"),
        },
      ],
    })
  );

  server.registerResource(
    "Sales Database",
    "mssql://local/db/sales",
    {
      title: "sales",
      description: "Entry resource for the sales database.",
      mimeType: "text/markdown",
    },
    async (uri) => ({
      contents: [
        {
          uri: uri.toString(),
          mimeType: "text/markdown",
          text: [
            "# Database: sales",
            "",
            "Common entry points:",
            "",
            "- mssql://local/db/sales/schemas",
            "- mssql://local/db/sales/schema/dbo/tables",
            "- mssql://local/db/sales/schema/dbo/views",
            "- mssql://local/db/sales/schema/dbo/procedures",
          ].join("\n"),
        },
      ],
    })
  );
}

这里的关键点是:静态注册的 resource 不需要覆盖整个数据库。它们更像“导航入口”。真正大量的数据库对象,应该交给 Resource Template 动态读取。

例如:

mssql://local/db/sales/schema/dbo/tables

可以返回:

# Tables in dbo

- dbo.Customers
- dbo.Orders
- dbo.OrderItems
- dbo.Products

而:

mssql://local/db/sales/schema/dbo/table/Orders

可以返回:

CREATE TABLE dbo.Orders (
    OrderId int NOT NULL PRIMARY KEY,
    CustomerId int NOT NULL,
    CreatedAt datetime2 NOT NULL,
    Status nvarchar(32) NOT NULL
);

-- Foreign Keys
-- FK_Orders_Customers: CustomerId -> dbo.Customers.CustomerId

-- Indexes
-- PK_Orders
-- IX_Orders_CustomerId
-- IX_Orders_CreatedAt

这种方式让 resources/list 保持轻量,同时保留资源浏览的层次感。

使用 Resource Templates 表达动态资源

大量数据库对象不适合全部预注册,但它们适合通过 Resource Template 暴露。Resource Template 可以告诉客户端:Server 支持某一类 URI 结构,客户端可以根据参数构造具体资源。

对于 SQL Server MCP,可以设计如下模板:

mssql://{server}/db/{database}
mssql://{server}/db/{database}/schema/{schema}
mssql://{server}/db/{database}/schema/{schema}/table/{table}
mssql://{server}/db/{database}/schema/{schema}/view/{view}
mssql://{server}/db/{database}/schema/{schema}/procedure/{procedure}
mssql://{server}/db/{database}/schema/{schema}/function/{function}
mssql://{server}/db/{database}/schema/{schema}/table/{table}/indexes
mssql://{server}/db/{database}/schema/{schema}/table/{table}/sample?limit={limit}

代码可以这样表达:

// resources/templates.ts
import {
  McpServer,
  ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";

export function registerSqlResourceTemplates(server: McpServer) {
  server.registerResource(
    "SQL Table",
    new ResourceTemplate(
      "mssql://{server}/db/{database}/schema/{schema}/table/{table}",
      {
        list: undefined,
        complete: {
          server: completeServers,
          database: completeDatabases,
          schema: completeSchemas,
          table: completeTables,
        },
      }
    ),
    {
      title: "SQL Table",
      description: "SQL Server table schema resource.",
      mimeType: "application/sql",
    },
    async (uri, variables) => {
      const ddl = await getTableDefinition({
        serverName: String(variables.server),
        database: String(variables.database),
        schema: String(variables.schema),
        table: String(variables.table),
      });

      return {
        contents: [
          {
            uri: uri.toString(),
            mimeType: "application/sql",
            text: ddl,
          },
        ],
      };
    }
  );

  server.registerResource(
    "SQL Procedure",
    new ResourceTemplate(
      "mssql://{server}/db/{database}/schema/{schema}/procedure/{procedure}",
      {
        list: undefined,
        complete: {
          server: completeServers,
          database: completeDatabases,
          schema: completeSchemas,
          procedure: completeProcedures,
        },
      }
    ),
    {
      title: "SQL Procedure",
      description: "SQL Server stored procedure definition resource.",
      mimeType: "application/sql",
    },
    async (uri, variables) => {
      const sql = await getProcedureDefinition({
        serverName: String(variables.server),
        database: String(variables.database),
        schema: String(variables.schema),
        procedure: String(variables.procedure),
      });

      return {
        contents: [
          {
            uri: uri.toString(),
            mimeType: "application/sql",
            text: sql,
          },
        ],
      };
    }
  );
}

这段代码体现了一个重要原则:table 和 procedure 本身就是 resource。它们不是 describe_table 工具返回的一段普通文本,而是有 URI、有 MIME 类型、有标题和描述的上下文对象。

但是需要注意,Resource Template 本身只说明“可以这样访问资源”,不等于客户端一定会自动提供完美的路径补全。路径补全、树形浏览、搜索体验,仍然取决于具体 Agent 客户端的实现。

用 Completion 支持类似文件路径的补全体验

如果希望用户输入 @mssql:// 时像输入文件路径一样逐层补全,MCP Server 需要配合实现 completion 能力。比如用户输入:

@mssql://

客户端可以请求 server 参数补全,Server 返回:

local
dev
prod

用户选择 local 后继续输入:

@mssql://local/db/

Server 再返回:

sales
hr
finance

继续选择 sales 后,Server 返回 schema:

dbo
reporting
audit

最后补全 table:

Customers
Orders
OrderItems
Products

对应代码可以先写成简单版本:

async function completeServers(value: string) {
  const servers = ["local", "dev", "prod"];
  return servers.filter((x) => x.startsWith(value));
}

async function completeDatabases(value: string, context?: any) {
  const serverName = context?.arguments?.server ?? "local";
  const databases = await listDatabases(serverName);
  return databases.filter((x) => x.startsWith(value));
}

async function completeSchemas(value: string, context?: any) {
  const serverName = context?.arguments?.server ?? "local";
  const database = context?.arguments?.database;

  if (!database) return [];

  const schemas = await listSchemas(serverName, database);
  return schemas.filter((x) => x.startsWith(value));
}

async function completeTables(value: string, context?: any) {
  const serverName = context?.arguments?.server ?? "local";
  const database = context?.arguments?.database;
  const schema = context?.arguments?.schema;

  if (!database || !schema) return [];

  const tables = await listTables(serverName, database, schema);
  return tables.filter((x) => x.startsWith(value));
}

async function completeProcedures(value: string, context?: any) {
  const serverName = context?.arguments?.server ?? "local";
  const database = context?.arguments?.database;
  const schema = context?.arguments?.schema;

  if (!database || !schema) return [];

  const procedures = await listProcedures(serverName, database, schema);
  return procedures.filter((x) => x.startsWith(value));
}

这个流程中,MCP Server 负责提供可补全的数据,Agent Client 负责识别用户正在输入资源引用,并决定何时调用补全接口、如何展示结果、是否缓存结果、是否显示成树形结构。

换句话说,MCP Server 提供“资源语义”和“补全能力”,但 @mssql:// 的最终交互体验,是 MCP Server 和 Agent Client 共同完成的。

仅靠 Resource 还不够:模型也需要主动探索资源

有一个更复杂但非常真实的场景:

用户:帮我看看 @mssql://local/db/sales/schema/dbo/procedure/GetOrders 为什么出错。

用户手动把 GetOrders 这个存储过程作为资源加入上下文。Agent 读取了 procedure 的代码,发现里面引用了 dbo.Orders 和 dbo.Customers。这时模型需要继续查看这些表的定义,才能判断错误原因。

问题来了:模型怎么知道 dbo.Orders 的资源 URI?又应该通过什么方式读取它?

从 MCP 的语义上看,resources/read 是 Client/Host 调用 MCP Server 的通用资源读取能力,而 Tool 才是模型主动调用的动作。如果 Agent Host 没有把通用 read_resource(uri) 暴露给模型,那么模型虽然知道自己需要更多上下文,却不一定能主动读取相关资源。

因此,SQL Server MCP 最好不要只提供静态 resources,还应该提供一组面向模型探索的工具,例如:

resolve_database_object(name, contextUri)
get_object_dependencies(objectUri)
get_referenced_by(objectUri)
search_database_objects(query, contextUri)
read_database_resource(uri)

这些工具不是为了取代 resources,而是为了让模型能够在分析过程中发现和请求更多 resources。

例如,模型看到存储过程里有 dbo.Orders,可以先调用对象解析工具:

server.registerTool(
  "resolve_database_object",
  {
    title: "Resolve SQL Object",
    description: "Resolve a SQL object name to a MCP resource URI.",
    inputSchema: {
      name: z.string(),
      contextUri: z.string().optional(),
    },
  },
  async ({ name, contextUri }) => {
    const object = await resolveObjectName(name, contextUri);

    return {
      content: [
        {
          type: "text",
          text: `Resolved ${name} to ${object.uri}`,
        },
        {
          type: "resource_link",
          uri: object.uri,
          name: object.name,
          title: object.title,
          description: object.description,
          mimeType: object.mimeType,
        },
      ],
      structuredContent: object,
    };
  }
);

这里的工具不是直接返回表结构,而是把 dbo.Orders 解析成一个真正的 resource URI。这样模型可以继续请求读取它,客户端也有机会显示这个资源链接。

Resource Bridge Tool:让工具结果仍然保持资源语义

这里最关键的是 read_database_resource(uri)。它看起来和 MCP 自带的 resources/read 有点重复,但两者服务的对象不同。

resources/read 是 Host/Client 读取资源的协议能力;read_database_resource(uri) 是模型可以主动调用的工具。它的意义不是重新发明资源读取,而是在当前客户端还没有通用模型可控资源读取能力时,提供一个桥接层。

不过,这个工具不应该返回普通文本。错误的做法是:

{
  "content": [
    {
      "type": "text",
      "text": "CREATE TABLE dbo.Orders (...)"
    }
  ]
}

这样客户端只能看到一次普通工具调用,很难知道 AI 实际上读取的是 dbo.Orders 这个资源。

更好的做法是让工具结果携带资源语义,例如返回 embedded resource 或 resource link:

server.registerTool(
  "read_database_resource",
  {
    title: "Read SQL Resource",
    description: "Read a SQL Server MCP resource by URI.",
    inputSchema: {
      uri: z.string(),
    },
  },
  async ({ uri }) => {
    const resource = await readSqlResourceByUri(uri);

    return {
      content: [
        {
          type: "resource",
          resource: {
            uri,
            mimeType: resource.mimeType,
            text: resource.text,
          },
        },
      ],
      structuredContent: {
        uri,
        kind: resource.kind,
        database: resource.database,
        schema: resource.schema,
        name: resource.name,
      },
    };
  }
);

对应返回结果可以理解成:

{
  "content": [
    {
      "type": "resource",
      "resource": {
        "uri": "mssql://local/db/sales/schema/dbo/table/Orders",
        "mimeType": "application/sql",
        "text": "CREATE TABLE dbo.Orders (...);"
      }
    }
  ],
  "structuredContent": {
    "uri": "mssql://local/db/sales/schema/dbo/table/Orders",
    "kind": "table",
    "database": "sales",
    "schema": "dbo",
    "name": "Orders"
  }
}

这样做的好处是,客户端有机会把它渲染成“AI 正在读取 dbo.Orders”,并且可以把 dbo.Orders 显示成可点击、可追踪的资源对象。即使某些客户端暂时没有完全支持这种展示方式,MCP Server 也已经提供了更标准的资源语义,而不是把所有东西降级成普通文本。

会话级资源:让分析结果也能被引用

除了数据库中原本存在的对象,Agent 工作过程中还会产生一些临时结果。例如诊断报告、依赖分析报告、查询计划解释、schema diff 等。这些结果也可以作为 session-scoped resource 暴露,而不是只作为一次性文本输出。

例如,diagnose_procedure_error 工具可以生成一份诊断报告,并返回一个资源链接:

const sessionResources = new Map<string, string>();

server.registerTool(
  "diagnose_procedure_error",
  {
    title: "Diagnose SQL Procedure Error",
    description: "Analyze a stored procedure error and create a diagnosis report.",
    inputSchema: {
      procedureUri: z.string(),
      errorMessage: z.string(),
    },
  },
  async ({ procedureUri, errorMessage }) => {
    const report = await buildDiagnosisReport(procedureUri, errorMessage);
    const reportUri = `mssql://session/diagnosis/${Date.now()}`;

    sessionResources.set(reportUri, report);

    server.registerResource(
      "SQL Diagnosis Report",
      reportUri,
      {
        title: "SQL Diagnosis Report",
        description: `Diagnosis report for ${procedureUri}`,
        mimeType: "text/markdown",
      },
      async (uri) => ({
        contents: [
          {
            uri: uri.toString(),
            mimeType: "text/markdown",
            text: sessionResources.get(uri.toString()) ?? "",
          },
        ],
      })
    );

    return {
      content: [
        {
          type: "text",
          text: "Created a diagnosis report.",
        },
        {
          type: "resource_link",
          uri: reportUri,
          name: "SQL Diagnosis Report",
          title: "SQL Diagnosis Report",
          description: `Diagnosis report for ${procedureUri}`,
          mimeType: "text/markdown",
        },
      ],
    };
  }
);

这样,工具生成的分析结果也不只是聊天里的普通文字,而是一个可以被客户端展示、再次读取、再次引用的资源对象。这对于长任务尤其重要,因为用户和模型都可能需要回到某个中间分析结果。

推荐的 SQL Server MCP 资源设计

一个比较完整的 SQL Server MCP 可以按下面方式组织。

首先,resources/list 只返回少量入口资源:

mssql://local
mssql://local/db/sales
mssql://local/db/sales/schemas
mssql://local/db/sales/schema/dbo
mssql://local/db/sales/schema/dbo/tables
mssql://local/db/sales/schema/dbo/views
mssql://local/db/sales/schema/dbo/procedures

其次,通过 resources/templates/list 暴露动态资源形状:

mssql://{server}/db/{database}
mssql://{server}/db/{database}/schema/{schema}
mssql://{server}/db/{database}/schema/{schema}/table/{table}
mssql://{server}/db/{database}/schema/{schema}/view/{view}
mssql://{server}/db/{database}/schema/{schema}/procedure/{procedure}
mssql://{server}/db/{database}/schema/{schema}/function/{function}
mssql://{server}/db/{database}/schema/{schema}/table/{table}/indexes

然后,实现 completion 支持逐层补全:

server -> database -> schema -> object type -> object name

最后,提供少量模型可调用的数据库语义工具:

resolve_database_object(name, contextUri)
get_object_dependencies(objectUri)
get_referenced_by(objectUri)
search_database_objects(query, contextUri)
read_database_resource(uri)
diagnose_procedure_error(procedureUri, errorMessage)
run_readonly_query(sql)

其中,resolve_database_object 可以把 Orders 解析成完整 URI:

{
  "name": "dbo.Orders",
  "kind": "table",
  "uri": "mssql://local/db/sales/schema/dbo/table/Orders"
}

get_object_dependencies 可以返回依赖资源:

{
  "object": {
    "uri": "mssql://local/db/sales/schema/dbo/procedure/GetOrders"
  },
  "dependencies": [
    {
      "name": "dbo.Orders",
      "kind": "table",
      "uri": "mssql://local/db/sales/schema/dbo/table/Orders"
    },
    {
      "name": "dbo.Customers",
      "kind": "table",
      "uri": "mssql://local/db/sales/schema/dbo/table/Customers"
    }
  ]
}

read_database_resource 则负责把某个 URI 读取成带资源语义的结果,而不是普通文本。

一个完整交互流程

假设用户提出问题:

帮我看看 @mssql://local/db/sales/schema/dbo/procedure/GetOrders 为什么出错。

比较理想的流程如下:

1. 用户通过 @ 引用 GetOrders。
2. Agent Client 调用 resources/read 读取 GetOrders。
3. 模型看到 GetOrders 中引用了 dbo.Orders 和 dbo.Customers。
4. 模型调用 get_object_dependencies,确认依赖对象。
5. 工具返回 dbo.Orders 和 dbo.Customers 的 resource URI。
6. 模型调用 read_database_resource 读取相关表定义。
7. 工具返回 embedded resource,而不是普通文本。
8. 客户端显示 AI 正在读取 dbo.Orders、dbo.Customers。
9. 模型结合 procedure 和 table schema 分析错误原因。

这个流程里,resources 负责让数据库对象成为可引用上下文,tools 负责让模型主动探索上下文,embedded resource / resource link 负责让工具调用仍然保留资源语义。三者结合起来,才能接近现代 AI 编程工具中“读取文件”的体验。

为什么不要只做 Tools

如果 SQL Server MCP 只提供 tools,例如:

list_tables
describe_table
get_procedure
search_columns

模型当然也能工作。但客户端看到的只是工具调用,用户体验会退化成:

AI called describe_table
AI called get_procedure
AI called search_columns

这和:

AI is reading dbo.Orders
AI is reading dbo.Customers
AI is reading dbo.GetOrders

是不一样的。

后者更接近现代 Agent IDE 的体验。用户知道 AI 正在看哪些对象,也可以点击这些对象查看详情。对于数据库、文档系统、企业知识库、代码仓库来说,这种“可追踪的上下文读取”非常重要。

为什么也不要只做 Resources

反过来,如果 SQL Server MCP 只提供 resources,也会遇到问题。用户可以手动引用 @procedure1,但模型分析过程中发现还需要 table1、view1、function1 时,不一定有办法主动读取它们。除非 Agent Host 提供了通用的模型可控资源读取能力,否则模型仍然需要 tools 来搜索、解析和请求相关资源。

所以更现实的结论不是“resources 和 tools 二选一”,而是:

Resources 负责表达对象。
Resource Templates 负责表达对象空间。
Completion 负责改善用户输入和资源发现体验。
Tools 负责让模型主动探索、解析和诊断。
ResourceLink / Embedded Resource 负责让工具结果保留资源语义。

结论

一个优雅的 SQL Server MCP,不应该把数据库对象全部塞进 resources/list,也不应该只用一堆普通 tools 暴露数据库能力。更好的设计是把数据库对象当作资源,把数据库操作当作工具,把两者通过 URI、ResourceLink 和 embedded resource 连接起来。

对于表、视图、存储过程、函数、索引等稳定对象,应该优先设计为 MCP resources。对于搜索、依赖分析、对象解析、错误诊断、只读 SQL 执行等动态行为,应该设计为 tools。对于模型主动读取资源的需求,可以提供 read_database_resource(uri) 这样的桥接工具,但它的返回结果应该携带 MCP 资源语义,而不是普通文本。

这样设计之后,SQL Server MCP 才不仅仅是一个“数据库查询工具集合”,而是一个真正适合 Agent 使用的上下文资源系统。它既支持用户通过 @ 引用数据库对象,也支持模型在分析过程中主动发现更多上下文,还为客户端提供了展示“AI 正在读取哪个数据库对象”的可能性。

posted on 2026-06-11 23:26  编写人生  阅读(213)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3