【MCP Node.js SDK 全栈进阶指南】中级篇(3):MCP高级资源设计
前言
在MCP TypeScript-SDK的初级篇中,我们介绍了资源开发的基础知识,包括静态资源与动态资源的创建、资源模板设计与参数提取,以及基本的资源列表与发现机制。随着应用规模的扩大和复杂性的提高,我们需要更加高级的资源设计方案来应对各种挑战。
本文作为中级篇的第三篇,将深入探讨MCP高级资源设计,包括复杂资源结构设计、资源分页与过滤、资源缓存策略以及大规模资源管理方案。通过学习这些高级技术,你将能够构建更加高效、灵活且可扩展的MCP资源系统,满足复杂企业级应用的需求。
一、复杂资源结构设计
在实际应用场景中,资源往往具有复杂的结构和关系,简单的扁平资源结构无法满足复杂业务需求。本节将探讨如何设计和实现复杂的资源结构。
1.1 嵌套资源设计
嵌套资源是指资源之间存在层级关系,子资源依附于父资源存在。嵌套资源设计可以更好地表达业务实体之间的从属关系。
import { McpServer } from '@modelcontextprotocol/sdk';
import { z } from 'zod';const server = new McpServer({name: 'nested-resource-server',description: '嵌套资源示例服务器',version: '1.0.0',
});// 创建公司资源
server.registerResource({name: 'companies',description: '公司列表资源',parameters: z.object({filter: z.string().optional().describe('过滤条件'),}),resolve: async ({ filter }) => {const companies = await fetchCompanies(filter);return {content: JSON.stringify(companies),metadata: {count: companies.length,filter: filter || 'none',}};}
});// 创建部门资源(作为公司的子资源)
server.registerResource({name: 'companies/:companyId/departments',description: '部门列表资源',parameters: z.object({companyId: z.string().describe('公司ID'),filter: z.string().optional().describe('过滤条件'),}),resolve: async ({ companyId, filter }) => {const departments = await fetchDepartments(companyId, filter);return {content: JSON.stringify(departments),metadata: {companyId,count: departments.length,filter: filter || 'none',}};}
});// 创建员工资源(作为部门的子资源)
server.registerResource({name: 'companies/:companyId/departments/:departmentId/employees',description: '员工列表资源',parameters: z.object({companyId: z.string().describe('公司ID'),departmentId: z.string().describe('部门ID'),filter: z.string().optional().describe('过滤条件'),}),resolve: async ({ companyId, departmentId, filter }) => {const employees = await fetchEmployees(companyId, departmentId, filter);return {content: JSON.stringify(employees),metadata: {companyId,departmentId,count: employees.length,filter: filter || 'none',}};}
});
1.2 资源关系与引用
在复杂系统中,资源之间往往存在各种关系,如一对一、一对多、多对多等。通过资源引用可以表达这些复杂关系,并允许客户端遍历相关资源。
// 使用URI引用表达资源之间的关系
server.registerResource({name: 'projects/:projectId',description: '项目详情',parameters: z.object({projectId: z.string().describe('项目ID'),}),resolve: async ({ projectId }) => {const project = await fetchProject(projectId);// 构建关联资源的URI引用const teamUri = `teams://${project.teamId}`;const tasksUri = `projects://${projectId}/tasks`;const documentsUri = `projects://${projectId}/documents`;return {content: JSON.stringify({...project,// 添加资源引用_links: {team: teamUri,tasks: tasksUri,documents: documentsUri,}}),metadata: {relatedResources: [teamUri, tasksUri, documentsUri],}};}
});
1.3 资源版本控制
对于频繁变化的资源,版本控制是确保一致性和兼容性的重要机制。
// 实现资源版本控制
server.registerResource({name: 'api-specs/:version',description: 'API规范文档',parameters: z.object({version: z.string().describe('API版本,格式为v1, v2等,或latest表示最新版本'),}),resolve: async ({ version }) => {// 处理特殊版本标识符const actualVersion = version === 'latest' ? await getLatestApiVersion(): version;const apiSpec = await fetchApiSpec(actualVersion);if (!apiSpec) {throw new Error(`API规范版本 ${actualVersion} 不存在`);}return {content: apiSpec.content,metadata: {version: actualVersion,publishedAt: apiSpec.publishedAt,isLatest: await isLatestVersion(actualVersion),previousVersion: apiSpec.previousVersion,nextVersion: apiSpec.nextVersion,}};}
});
1.4 多语言资源支持
全球化应用需要支持多种语言的资源内容,MCP可以轻松实现多语言资源。
// 实现多语言资源支持
server.registerResource({name: 'localized-content/:contentId',description: '多语言内容资源',parameters: z.object({contentId: z.string().describe('内容ID'),language: z.string().default('en').describe('语言代码,如en, zh-CN, ja等'),}),resolve: async ({ contentId, language }) => {const content = await fetchLocalizedContent(contentId, language);// 如果请求的语言版本不存在,返回默认语言版本const actualContent = content || await fetchLocalizedContent(contentId, 'en');if (!actualContent) {throw new Error(`内容 ${contentId} 不存在`);}return {content: actualContent.content,metadata: {contentId,language: actualContent.language, // 返回实际使用的语言availableLanguages: await getAvailableLanguages(contentId),defaultLanguage: 'en',translatedAt: actualContent.translatedAt,}};}
});
二、资源分页与过滤
当资源数据量很大时,一次性加载所有资源会导致性能问题。资源分页和过滤机制可以帮助客户端高效地访问大型数据集。
2.1 基于游标的分页实现
MCP推荐使用基于游标的分页机制,而不是传统的基于页码的分页。基于游标的分页更加高效,特别是对于大型数据集,且能够有效处理数据集动态变化的情况。
import { McpServer } from '@modelcontextprotocol/sdk';
import { z } from 'zod';const server = new McpServer({name: 'pagination-server',description: '分页资源示例服务器',version: '1.0.0',
});// 基于游标的分页实现
server.registerResource({name: 'articles',description: '文章列表资源',parameters: z.object({limit: z.number().int().min(1).max(100).default(20).describe('每页条数'),cursor: z.string().optional().describe('分页游标'),sortBy: z.enum(['date', 'title', 'views']).default('date').describe('排序字段'),sortOrder: z.enum(['asc', 'desc']).default('desc').describe('排序方向'),}),resolve: async ({ limit, cursor, sortBy, sortOrder }) => {// 解析游标(如果有)let cursorData = { offset: 0 };if (cursor) {try {cursorData = JSON.parse(Buffer.from(cursor, 'base64').toString());} catch (error) {throw new Error('无效的分页游标');}}// 查询数据const { articles, totalCount } = await fetchArticles({offset: cursorData.offset,limit,sortBy,sortOrder,});// 计算下一页游标const nextOffset = cursorData.offset + articles.length;const hasNextPage = nextOffset < totalCount;const nextCursor = hasNextPage? Buffer.from(JSON.stringify({ offset: nextOffset })).toString('base64'): null;return {content: JSON.stringify(articles),metadata: {pagination: {totalCount,returnedCount: articles.length,hasNextPage,nextCursor,},sortBy,sortOrder,}};}
});
2.2 高级过滤与搜索
过滤和搜索机制允许客户端精确获取所需的资源子集,减少不必要的数据传输。
// 实现高级过滤与搜索
server.registerResource({name: 'products',description: '产品列表资源',parameters: z.object({cursor: z.string().optional().describe('分页游标'),limit: z.number().int().min(1).max(100).default(20).describe('每页条数'),// 高级过滤参数category: z.string().optional().describe('产品类别'),minPrice: z.number().optional().describe('最低价格'),maxPrice: z.number().optional().describe('最高价格'),inStock: z.boolean().optional().describe('是否有库存'),tags: z.array(z.string()).optional().describe('标签列表'),// 搜索参数query: z.string().optional().describe('搜索关键词'),searchFields: z.array(z.enum(['name', 'description', 'sku'])).optional().describe('搜索字段'),}),resolve: async (params) => {// 构建过滤条件const filters = [