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}`,
},更多推荐
所有评论(0)