Resource 与 Tool 的边界MCP 中的 Resource 更适合表达“已经存在、可以被引用、可以被读取的上下文对象”。例如mssql://local/db/sales/schema/dbo/table/Ordersmssql://local/db/sales/schema/dbo/procedure/GetOrdersmssql://local/db/sales/schema/dbo/view/OrderSummarymssql://local/db/sales/schema/dbo/table/Orders/indexes这些 URI 表示的是数据库中的稳定对象。用户可以通过 引用它们Agent 客户端也可以通过资源浏览、搜索、补全等方式把它们加入上下文。相比之下Tool 更适合表达“动作”。例如搜索对象、解析依赖、执行只读查询、诊断 SQL 错误、分析执行计划等都更适合作为工具。因此一个 SQL Server MCP 不应该只暴露 tools也不应该把数据库中所有对象一股脑注册为 resources。更合理的设计是用 resources 表达数据库对象用 resource templates 表达可参数化的资源路径用 tools 帮助模型发现、解析和读取相关资源。代码层面可以先把资源注册集中起来避免所有逻辑散落在 server 初始化代码中// resources/index.tsimport { 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://localmssql://local/db/salesmssql://local/db/sales/schemasmssql://local/db/sales/schema/dbomssql://local/db/sales/schema/dbo/tablesmssql://local/db/sales/schema/dbo/viewsmssql://local/db/sales/schema/dbo/procedures这些资源更像目录入口。用户或客户端读取 mssql://local/db/sales/schema/dbo/tables 时Server 可以返回该 schema 下的表清单读取具体表 URI 时Server 再返回表结构、列、主键、外键、索引摘要、行数估计等内容。入口资源可以这样写// resources/entries.tsimport { 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 dbodbo.Customersdbo.Ordersdbo.OrderItemsdbo.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}/indexesmssql://{server}/db/{database}/schema/{schema}/table/{table}/sample?limit{limit}代码可以这样表达// resources/templates.tsimport {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 返回localdevprod用户选择 local 后继续输入mssql://local/db/Server 再返回saleshrfinance继续选择 sales 后Server 返回 schemadboreportingaudit最后补全 tableCustomersOrdersOrderItemsProducts对应代码可以先写成简单版本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 linkserver.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 Mapstring, 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://localmssql://local/db/salesmssql://local/db/sales/schemasmssql://local/db/sales/schema/dbomssql://local/db/sales/schema/dbo/tablesmssql://local/db/sales/schema/dbo/viewsmssql://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 为什么出错。比较理想的流程如下用户通过 引用 GetOrders。Agent Client 调用 resources/read 读取 GetOrders。模型看到 GetOrders 中引用了 dbo.Orders 和 dbo.Customers。模型调用 get_object_dependencies确认依赖对象。工具返回 dbo.Orders 和 dbo.Customers 的 resource URI。模型调用 read_database_resource 读取相关表定义。工具返回 embedded resource而不是普通文本。客户端显示 AI 正在读取 dbo.Orders、dbo.Customers。模型结合 procedure 和 table schema 分析错误原因。这个流程里resources 负责让数据库对象成为可引用上下文tools 负责让模型主动探索上下文embedded resource / resource link 负责让工具调用仍然保留资源语义。三者结合起来才能接近现代 AI 编程工具中“读取文件”的体验。为什么不要只做 Tools如果 SQL Server MCP 只提供 tools例如list_tablesdescribe_tableget_proceduresearch_columns模型当然也能工作。但客户端看到的只是工具调用用户体验会退化成AI called describe_tableAI called get_procedureAI called search_columns这和AI is reading dbo.OrdersAI is reading dbo.CustomersAI 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 使用的上下文资源系统。它