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/listSQL 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 返回 schemadbo reporting audit最后补全 tableCustomers 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}, },