@faapi/schema 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +11 -6
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createMcpServer, createMcpNodeHandler } from "@faapi/mcp";
|
|
|
7
7
|
import ts from "typescript";
|
|
8
8
|
import {
|
|
9
9
|
collectRouteSchemaSources,
|
|
10
|
-
|
|
10
|
+
createPrograms,
|
|
11
11
|
invalidateProgramCache,
|
|
12
12
|
extractTypeInfo,
|
|
13
13
|
resolveTypeNode,
|
|
@@ -24,9 +24,15 @@ function buildRouteSchemas(routes, rootDir) {
|
|
|
24
24
|
for (const source of sources) {
|
|
25
25
|
filePathMap.set(source.urlPath, source.filePath);
|
|
26
26
|
}
|
|
27
|
+
const programByFile = createPrograms([...new Set(filePathMap.values())]);
|
|
27
28
|
return routes.map((route) => {
|
|
28
29
|
const inputs = extractInputSchemas(route, sourceMap);
|
|
29
|
-
const
|
|
30
|
+
const filePath = filePathMap.get(route.urlPath);
|
|
31
|
+
const output = extractOutputSchema(
|
|
32
|
+
route,
|
|
33
|
+
filePath,
|
|
34
|
+
filePath ? programByFile.get(filePath) : void 0
|
|
35
|
+
);
|
|
30
36
|
return {
|
|
31
37
|
method: route.method,
|
|
32
38
|
path: route.urlPath,
|
|
@@ -64,10 +70,9 @@ function extractInputSchemas(route, sourceMap) {
|
|
|
64
70
|
}
|
|
65
71
|
return inputs;
|
|
66
72
|
}
|
|
67
|
-
function extractOutputSchema(route, filePath) {
|
|
68
|
-
if (!filePath) return null;
|
|
73
|
+
function extractOutputSchema(route, filePath, program) {
|
|
74
|
+
if (!filePath || !program) return null;
|
|
69
75
|
try {
|
|
70
|
-
const program = createProgram(filePath);
|
|
71
76
|
const sourceFile = program.getSourceFile(filePath);
|
|
72
77
|
if (!sourceFile) return null;
|
|
73
78
|
const checker = program.getTypeChecker();
|
|
@@ -249,7 +254,7 @@ function createSchemaServer(getRoutes, rootDir) {
|
|
|
249
254
|
}
|
|
250
255
|
function registerResources(schemas) {
|
|
251
256
|
for (const uri of registeredUris) {
|
|
252
|
-
mcp.removeResource(uri);
|
|
257
|
+
mcp.removeResource(uri, { silent: true });
|
|
253
258
|
}
|
|
254
259
|
registeredUris.clear();
|
|
255
260
|
for (const route of schemas) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemaServer.ts","../src/routeSchema.ts"],"sourcesContent":["/**\n * faapi Schema Server — 通过 MCP 协议以 resource 形式暴露路由 schema 供 AI 助手查询\n *\n * 基于 @faapi/mcp(纯手写 MCP Server SDK),不依赖 @modelcontextprotocol/sdk。\n * 通过 faapi 插件机制挂载到 /mcp 端点,AI 助手通过 Streamable HTTP 连接。\n *\n * 提供两种 MCP 能力:\n * - resources: 每个路由注册为静态 resource + by-method resourceTemplate\n * - completion: 为 resource template 的 method 参数提供补全\n *\n * 不注册 tool——查 schema 是读数据(resource 语义),不是执行动作(tool 语义)。\n * resource 还有 AI 客户端原生 UI、可缓存、支持 subscription 等 tool 做不到的优势。\n */\n\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { createMcpServer, createMcpNodeHandler, type McpServer } from '@faapi/mcp';\nimport type { RouteManifest, RouteInfo, FaapiPlugin, PluginContext } from '@faapi/faapi';\nimport { buildRouteSchemas } from './routeSchema';\n\n/** MCP 端点路径 */\nconst MCP_PATH = '/mcp';\n\n/** 合法 HTTP 方法集合(用于 template read 校验 + completion 候选值) */\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const;\n\n/**\n * 从 package.json 读取版本号\n *\n * 通过 import.meta.url 解析 ../package.json,dev 模式下指向源文件所在包根目录,\n * prod 模式下指向 dist/ 同级的包根目录。模块加载时一次性读取。\n */\nfunction readPackageVersion(): string {\n const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };\n return pkg.version ?? '0.0.0';\n}\n\n/** 构造路由 resource 的 URI */\nfunction routeToUri(route: { method: string; path: string }): string {\n return `faapi://route/${route.method}${route.path}`;\n}\n\n/**\n * 创建 Schema MCP Server\n *\n * 注册:\n * - 每个路由一个静态 resource(URI: faapi://route/{METHOD}{PATH})\n * - 1 个 resourceTemplate(faapi://routes/by-method/{method},按方法过滤)\n * - 1 个 completion(为 template 的 method 参数提供候选值)\n *\n * @param getRoutes 返回最新路由清单的 getter(dev reloadRoutes 后返回更新后的数组)\n * @param rootDir 项目根目录(用于 AST 分析解析源文件)\n */\nexport function createSchemaServer(getRoutes: () => RouteManifest, rootDir: string): McpServer {\n const mcp = createMcpServer({\n name: 'faapi-schema',\n version: readPackageVersion(),\n // 路由变化时主动推送 notifications/resources/list_changed\n resourcesListChanged: true,\n });\n\n // 缓存 route schemas:通过路由数组引用比较检测变更\n let cachedRoutes: RouteManifest | null = null;\n let cachedSchemas: RouteInfo[] | null = null;\n // 已注册的 resource URI 集合(变更时先 remove 再注册,避免重复注册抛错)\n const registeredUris = new Set<string>();\n // 标记是否已首次注册(首次无需推送 list_changed,因为没有 session)\n let resourceRegistered = false;\n\n function getSchemas(): RouteInfo[] {\n const currentRoutes = getRoutes();\n if (currentRoutes !== cachedRoutes || !cachedSchemas) {\n cachedRoutes = currentRoutes;\n cachedSchemas = buildRouteSchemas(currentRoutes, rootDir);\n registerResources(cachedSchemas);\n // 非首次注册时通知客户端列表变更\n if (resourceRegistered) {\n mcp.notifyResourcesListChanged();\n }\n resourceRegistered = true;\n }\n return cachedSchemas;\n }\n\n /** 重新注册所有静态 resource(先清空旧的,再注册新的) */\n function registerResources(schemas: RouteInfo[]): void {\n // 清空旧 resource\n for (const uri of registeredUris) {\n mcp.removeResource(uri);\n }\n registeredUris.clear();\n\n // 注册新 resource\n for (const route of schemas) {\n const uri = routeToUri(route);\n // 闭包捕获当前 route,避免循环变量引用问题\n const captured = route;\n mcp.resource(uri, {\n name: `${route.method} ${route.path}`,\n mimeType: 'application/json',\n read: async () => ({\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(captured, null, 2),\n },\n ],\n }),\n });\n registeredUris.add(uri);\n }\n }\n\n // ─── ResourceTemplate: 按方法过滤路由 ─────────────────\n mcp.resourceTemplate('faapi://routes/by-method/{method}', {\n name: 'routes-by-method',\n description: '按 HTTP 方法过滤路由,返回该方法的所有路由列表',\n read: async (uri, params) => {\n const method = (params.method ?? '').toUpperCase();\n // 不合法的方法返回空数组(不抛错,避免 InternalError;客户端拿不到路由自然知道方法无效)\n if (!HTTP_METHODS.includes(method as (typeof HTTP_METHODS)[number])) {\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: '[]',\n },\n ],\n };\n }\n const schemas = getSchemas().filter((r) => r.method === method);\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(schemas, null, 2),\n },\n ],\n };\n },\n });\n\n // ─── Completion: method 参数补全 ──────────────────────\n mcp.completion(\n { type: 'ref/resource', uri: 'faapi://routes/by-method/{method}' },\n 'method',\n (value) => {\n const upper = value.toUpperCase();\n return {\n values: HTTP_METHODS.filter((m) => m.startsWith(upper)),\n };\n },\n );\n\n // 触发首次 schemas 构建 + resource 注册\n // 必须在返回前调用,否则 resources/list 会返回空(懒加载不会在 list 时触发)\n getSchemas();\n\n return mcp;\n}\n\n/**\n * faapi 插件入口\n *\n * 在 faapi.config.ts 中声明:\n * ```ts\n * export default {\n * plugins: ['@faapi/schema'],\n * } satisfies FaapiConfig;\n * ```\n *\n * 插件在 /mcp 路径挂载 MCP 端点,AI 助手通过 Streamable HTTP 连接。\n */\nexport default {\n name: '@faapi/schema',\n setup(ctx: PluginContext) {\n const mcp = createSchemaServer(ctx.getRoutes, ctx.rootDir);\n const nodeHandler = createMcpNodeHandler(mcp);\n\n // 拦截 /mcp 路径的请求,交给 MCP handler 处理\n ctx.wrapHandler?.((original) => (req, res) => {\n const url = new URL(req.url ?? '/', 'http://localhost');\n if (url.pathname === MCP_PATH) {\n return nodeHandler(req, res);\n }\n return original(req, res);\n });\n\n console.log(`- Schema server enabled at ${MCP_PATH} (Streamable HTTP)`);\n },\n} satisfies FaapiPlugin;\n","/**\n * 从路由清单生成接口描述信息(含输入参数与响应类型)\n *\n * 直接调用主包 `collectRouteSchemaSources` 执行 AST 分析,提取每个路由 handler 的\n * 输入参数类型与返回类型,无需依赖运行时 schemaRegistry。\n *\n * 响应类型提取独立实现 Promise 解包逻辑,不修改主包 resolveTypeNode\n * (主包在运行时校验场景下遇到 Promise 会抛错,响应类型提取场景需解包取 T)。\n */\n\nimport ts from 'typescript';\nimport {\n collectRouteSchemaSources,\n createProgram,\n invalidateProgramCache,\n extractTypeInfo,\n resolveTypeNode,\n getInputTypeForMethod,\n SchemaExtractionError,\n type RouteManifest,\n type RouteInfo,\n type RouteInputSchema,\n type RouteOutputSchema,\n type RouteParamSchema,\n type RuntimeType,\n type PropertyType,\n} from '@faapi/faapi';\n\n/**\n * 从路由清单生成接口描述信息\n *\n * @param routes 路由清单\n * @param rootDir 项目根目录(用于解析源文件绝对路径)\n */\nexport function buildRouteSchemas(routes: RouteManifest, rootDir: string): RouteInfo[] {\n const { sources } = collectRouteSchemaSources(routes, rootDir);\n\n // 按 urlPath#schemaName 索引(schemaName = `${method}${inputType}`,如 GETQuery)\n const sourceMap = new Map<string, (typeof sources)[number]>();\n for (const source of sources) {\n sourceMap.set(`${source.urlPath}#${source.schemaName}`, source);\n }\n\n // 按 urlPath 索引源文件绝对路径(用于响应类型提取创建 program)\n const filePathMap = new Map<string, string>();\n for (const source of sources) {\n filePathMap.set(source.urlPath, source.filePath);\n }\n\n return routes.map((route) => {\n const inputs = extractInputSchemas(route, sourceMap);\n const output = extractOutputSchema(route, filePathMap.get(route.urlPath));\n return {\n method: route.method,\n path: route.urlPath,\n filePath: route.filePath,\n isDynamic: route.isDynamic,\n inputs,\n output,\n };\n });\n}\n\n/**\n * 提取一个路由的所有输入 schema\n *\n * 从 sourceMap 查询 AST 提取结果;无类型声明时 properties 为空。\n * 动态路由无 params 类型声明时,用 paramNames 兜底为 string[]。\n */\nfunction extractInputSchemas(\n route: { method: string; urlPath: string; isDynamic: boolean; paramNames: string[] },\n sourceMap: Map<\n string,\n { schemaName: string; typeInfo: { name: string; properties: PropertyType[] } | null }\n >,\n): RouteInputSchema[] {\n const inputs: RouteInputSchema[] = [];\n\n // 主输入(query 或 body)\n const inputType = getInputTypeForMethod(route.method);\n const schemaName = `${route.method.toUpperCase()}${capitalize(inputType)}`;\n const source = sourceMap.get(`${route.urlPath}#${schemaName}`);\n\n if (source?.typeInfo) {\n inputs.push({\n source: inputType,\n schemaName: source.typeInfo.name,\n properties: toParamSchemas(source.typeInfo.properties),\n });\n } else {\n // 无类型声明\n inputs.push({\n source: inputType,\n schemaName: null,\n properties: [],\n });\n }\n\n // 动态路由参数:collectRouteSchemaSources 不提取 params 类型,用 paramNames 兜底\n if (route.isDynamic && route.paramNames.length > 0) {\n inputs.push({\n source: 'params',\n schemaName: null,\n properties: route.paramNames.map((name) => ({ name, type: 'string', required: true })),\n });\n }\n\n return inputs;\n}\n\n/**\n * 提取路由的响应类型\n *\n * 流程:\n * 1. 用 TypeScript Compiler API 定位 handler 函数节点\n * 2. 读取 node.type(显式返回类型注解)\n * 3. 无注解 → 返回 null\n * 4. 解包 Promise<T>(若存在)→ 解析 T\n * 5. void/Promise<void> → 返回 null\n * 6. 解析失败 catch SchemaExtractionError → 降级返回 null(不阻断整条路由 schema 构建)\n *\n * @param route 路由信息\n * @param filePath 源文件绝对路径(已由 collectRouteSchemaSources 解析)\n */\nfunction extractOutputSchema(\n route: { method: string; urlPath: string },\n filePath: string | undefined,\n): RouteOutputSchema | null {\n if (!filePath) return null;\n\n try {\n const program = createProgram(filePath);\n const sourceFile = program.getSourceFile(filePath);\n if (!sourceFile) return null;\n\n const checker = program.getTypeChecker();\n\n // 定位 handler 函数声明(export function GET/POST/...)\n const handlerName = route.method.toUpperCase();\n let returnTypeNode: ts.TypeNode | undefined;\n ts.forEachChild(sourceFile, (node) => {\n if (returnTypeNode) return;\n if (ts.isFunctionDeclaration(node) && node.name?.text === handlerName) {\n returnTypeNode = node.type;\n }\n });\n\n if (!returnTypeNode) return null;\n\n // 解包 Promise<T>\n const unwrapped = unwrapPromise(returnTypeNode);\n if (unwrapped === null) return null; // void\n\n // 解析解包后的类型\n const runtimeType = resolveTypeNode(unwrapped, checker);\n return runtimeTypeToOutput(unwrapped, runtimeType, program, filePath, checker);\n } catch (err) {\n // 解析失败降级为 null,不阻断整条路由 schema 构建\n if (err instanceof SchemaExtractionError) return null;\n throw err;\n }\n}\n\n/**\n * 解包 Promise<T> → T\n *\n * - Promise<T> → T\n * - Promise<void> → null(表示无响应类型)\n * - 非 Promise 类型 → 原样返回\n *\n * 仅解包一层,不递归(Promise<Promise<T>> 实际不会出现)。\n */\nfunction unwrapPromise(typeNode: ts.TypeNode): ts.TypeNode | null {\n if (!ts.isTypeReferenceNode(typeNode)) return typeNode;\n\n const typeName = typeNode.typeName.getText();\n if (typeName !== 'Promise') return typeNode;\n\n // Promise 无类型参数 → 视为 Promise<any> → 原样返回(any 由 resolveTypeNode 处理)\n const arg = typeNode.typeArguments?.[0];\n if (!arg) return typeNode;\n\n // Promise<void> → null\n if (ts.isTypeNode(arg) && arg.kind === ts.SyntaxKind.VoidKeyword) return null;\n\n return arg;\n}\n\n/**\n * 将 RuntimeType + 原始类型节点转换为 RouteOutputSchema\n *\n * - 命名类型引用(TypeReference)→ 调 extractTypeInfo 提取完整结构 → properties 来自 typeInfo\n * - 数组类型 T[](TypeReference Array<T>)→ 取元素类型名,提取元素结构\n * - 内联对象字面量(TypeLiteralNode)→ 直接从 runtimeType.properties 提取\n * - 其他(基础类型/联合/元组等)→ properties 为空,schemaName 为 null\n */\nfunction runtimeTypeToOutput(\n typeNode: ts.TypeNode,\n runtimeType: RuntimeType,\n program: ts.Program,\n filePath: string,\n _checker: ts.TypeChecker,\n): RouteOutputSchema {\n // 内联对象字面量 → 直接从 runtimeType 提取 properties\n if (ts.isTypeLiteralNode(typeNode)) {\n if (runtimeType.kind === 'object') {\n return {\n schemaName: null,\n properties: toParamSchemas(runtimeType.properties),\n };\n }\n }\n\n // 数组类型 T[](ArrayTypeNode)→ 取元素类型提取结构\n if (ts.isArrayTypeNode(typeNode) && runtimeType.kind === 'array') {\n const element = runtimeType.element;\n // 元素是命名类型引用 → 提取元素结构\n if (element.kind === 'ref') {\n const typeInfo = extractTypeInfo(program, filePath, element.name);\n if (typeInfo) {\n return {\n schemaName: element.name,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n // 元素已被 checker 展开为 object(resolveTypeNode 默认行为)→ 尝试从 AST 节点拿命名\n if (element.kind === 'object') {\n const elementNode = typeNode.elementType;\n if (elementNode && ts.isTypeReferenceNode(elementNode)) {\n const elementName = elementNode.typeName.getText();\n const typeInfo = extractTypeInfo(program, filePath, elementName);\n if (typeInfo) {\n return {\n schemaName: elementName,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n // 内联对象数组({ id: number }[])\n return {\n schemaName: null,\n properties: toParamSchemas(element.properties),\n };\n }\n // 数组元素非命名类型(如 string[]、number[])\n return { schemaName: null, properties: [] };\n }\n\n // 命名类型引用 → 提取完整结构\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName.getText();\n\n // 数组类型 Array<T>(TypeReference Array<T>)→ 取元素类型名提取结构\n if (runtimeType.kind === 'array') {\n const element = runtimeType.element;\n if (element.kind === 'ref') {\n const typeInfo = extractTypeInfo(program, filePath, element.name);\n if (typeInfo) {\n return {\n schemaName: element.name,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n // 数组元素是内联对象({ id: number }[])\n if (element.kind === 'object') {\n return {\n schemaName: null,\n properties: toParamSchemas(element.properties),\n };\n }\n // 数组元素非命名类型(如 string[]、number[])\n return { schemaName: null, properties: [] };\n }\n\n // 命名类型引用(interface/type alias)\n if (runtimeType.kind === 'ref' || runtimeType.kind === 'object') {\n const typeInfo = extractTypeInfo(program, filePath, typeName);\n if (typeInfo) {\n return {\n schemaName: typeName,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n }\n\n // 其他类型(基础类型/联合/元组等)\n return { schemaName: null, properties: [] };\n}\n\n/**\n * 首字母大写\n */\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n/**\n * 将 PropertyType[] 转换为 RouteParamSchema[]\n */\nfunction toParamSchemas(properties: PropertyType[]): RouteParamSchema[] {\n return properties.map((prop) => ({\n name: prop.name,\n type: runtimeTypeToString(prop.type),\n required: !prop.optional,\n }));\n}\n\n/**\n * 将 RuntimeType 转为可读字符串\n */\nfunction runtimeTypeToString(type: RuntimeType): string {\n switch (type.kind) {\n case 'string':\n case 'number':\n case 'boolean':\n case 'bigint':\n case 'null':\n case 'undefined':\n case 'date':\n return type.kind;\n case 'literal':\n return JSON.stringify(type.value);\n case 'array':\n return `${runtimeTypeToString(type.element)}[]`;\n case 'tuple':\n return `[${type.elements.map((e) => (e.rest ? '...' : '') + runtimeTypeToString(e.type) + (e.optional ? '?' : '')).join(', ')}]`;\n case 'object':\n return 'object';\n case 'union':\n return type.members.map(runtimeTypeToString).join(' | ');\n case 'record':\n return `Record<${runtimeTypeToString(type.key)}, ${runtimeTypeToString(type.value)}>`;\n case 'ref':\n return type.name;\n case 'any':\n case 'unknown':\n return 'unknown';\n default:\n return 'unknown';\n }\n}\n\n// 显式导出 invalidateProgramCache 便于测试清理缓存\nexport { invalidateProgramCache };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/schemaServer.ts","../src/routeSchema.ts"],"sourcesContent":["/**\n * faapi Schema Server — 通过 MCP 协议以 resource 形式暴露路由 schema 供 AI 助手查询\n *\n * 基于 @faapi/mcp(纯手写 MCP Server SDK),不依赖 @modelcontextprotocol/sdk。\n * 通过 faapi 插件机制挂载到 /mcp 端点,AI 助手通过 Streamable HTTP 连接。\n *\n * 提供两种 MCP 能力:\n * - resources: 每个路由注册为静态 resource + by-method resourceTemplate\n * - completion: 为 resource template 的 method 参数提供补全\n *\n * 不注册 tool——查 schema 是读数据(resource 语义),不是执行动作(tool 语义)。\n * resource 还有 AI 客户端原生 UI、可缓存、支持 subscription 等 tool 做不到的优势。\n */\n\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { createMcpServer, createMcpNodeHandler, type McpServer } from '@faapi/mcp';\nimport type { RouteManifest, RouteInfo, FaapiPlugin, PluginContext } from '@faapi/faapi';\nimport { buildRouteSchemas } from './routeSchema';\n\n/** MCP 端点路径 */\nconst MCP_PATH = '/mcp';\n\n/** 合法 HTTP 方法集合(用于 template read 校验 + completion 候选值) */\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'] as const;\n\n/**\n * 从 package.json 读取版本号\n *\n * 通过 import.meta.url 解析 ../package.json,dev 模式下指向源文件所在包根目录,\n * prod 模式下指向 dist/ 同级的包根目录。模块加载时一次性读取。\n */\nfunction readPackageVersion(): string {\n const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };\n return pkg.version ?? '0.0.0';\n}\n\n/** 构造路由 resource 的 URI */\nfunction routeToUri(route: { method: string; path: string }): string {\n return `faapi://route/${route.method}${route.path}`;\n}\n\n/**\n * 创建 Schema MCP Server\n *\n * 注册:\n * - 每个路由一个静态 resource(URI: faapi://route/{METHOD}{PATH})\n * - 1 个 resourceTemplate(faapi://routes/by-method/{method},按方法过滤)\n * - 1 个 completion(为 template 的 method 参数提供候选值)\n *\n * @param getRoutes 返回最新路由清单的 getter(dev reloadRoutes 后返回更新后的数组)\n * @param rootDir 项目根目录(用于 AST 分析解析源文件)\n */\nexport function createSchemaServer(getRoutes: () => RouteManifest, rootDir: string): McpServer {\n const mcp = createMcpServer({\n name: 'faapi-schema',\n version: readPackageVersion(),\n // 路由变化时主动推送 notifications/resources/list_changed\n resourcesListChanged: true,\n });\n\n // 缓存 route schemas:通过路由数组引用比较检测变更\n let cachedRoutes: RouteManifest | null = null;\n let cachedSchemas: RouteInfo[] | null = null;\n // 已注册的 resource URI 集合(变更时先 remove 再注册,避免重复注册抛错)\n const registeredUris = new Set<string>();\n // 标记是否已首次注册(首次无需推送 list_changed,因为没有 session)\n let resourceRegistered = false;\n\n function getSchemas(): RouteInfo[] {\n const currentRoutes = getRoutes();\n if (currentRoutes !== cachedRoutes || !cachedSchemas) {\n cachedRoutes = currentRoutes;\n cachedSchemas = buildRouteSchemas(currentRoutes, rootDir);\n registerResources(cachedSchemas);\n // 非首次注册时通知客户端列表变更\n if (resourceRegistered) {\n mcp.notifyResourcesListChanged();\n }\n resourceRegistered = true;\n }\n return cachedSchemas;\n }\n\n /** 重新注册所有静态 resource(先清空旧的,再注册新的) */\n function registerResources(schemas: RouteInfo[]): void {\n // 清空旧 resource——silent 跳过逐次广播,由 getSchemas 末尾统一 list_changed 一次\n for (const uri of registeredUris) {\n mcp.removeResource(uri, { silent: true });\n }\n registeredUris.clear();\n\n // 注册新 resource\n for (const route of schemas) {\n const uri = routeToUri(route);\n // 闭包捕获当前 route,避免循环变量引用问题\n const captured = route;\n mcp.resource(uri, {\n name: `${route.method} ${route.path}`,\n mimeType: 'application/json',\n read: async () => ({\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(captured, null, 2),\n },\n ],\n }),\n });\n registeredUris.add(uri);\n }\n }\n\n // ─── ResourceTemplate: 按方法过滤路由 ─────────────────\n mcp.resourceTemplate('faapi://routes/by-method/{method}', {\n name: 'routes-by-method',\n description: '按 HTTP 方法过滤路由,返回该方法的所有路由列表',\n read: async (uri, params) => {\n const method = (params.method ?? '').toUpperCase();\n // 不合法的方法返回空数组(不抛错,避免 InternalError;客户端拿不到路由自然知道方法无效)\n if (!HTTP_METHODS.includes(method as (typeof HTTP_METHODS)[number])) {\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: '[]',\n },\n ],\n };\n }\n const schemas = getSchemas().filter((r) => r.method === method);\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(schemas, null, 2),\n },\n ],\n };\n },\n });\n\n // ─── Completion: method 参数补全 ──────────────────────\n mcp.completion(\n { type: 'ref/resource', uri: 'faapi://routes/by-method/{method}' },\n 'method',\n (value) => {\n const upper = value.toUpperCase();\n return {\n values: HTTP_METHODS.filter((m) => m.startsWith(upper)),\n };\n },\n );\n\n // 触发首次 schemas 构建 + resource 注册\n // 必须在返回前调用,否则 resources/list 会返回空(懒加载不会在 list 时触发)\n getSchemas();\n\n return mcp;\n}\n\n/**\n * faapi 插件入口\n *\n * 在 faapi.config.ts 中声明:\n * ```ts\n * export default {\n * plugins: ['@faapi/schema'],\n * } satisfies FaapiConfig;\n * ```\n *\n * 插件在 /mcp 路径挂载 MCP 端点,AI 助手通过 Streamable HTTP 连接。\n */\nexport default {\n name: '@faapi/schema',\n setup(ctx: PluginContext) {\n const mcp = createSchemaServer(ctx.getRoutes, ctx.rootDir);\n const nodeHandler = createMcpNodeHandler(mcp);\n\n // 拦截 /mcp 路径的请求,交给 MCP handler 处理\n ctx.wrapHandler?.((original) => (req, res) => {\n const url = new URL(req.url ?? '/', 'http://localhost');\n if (url.pathname === MCP_PATH) {\n return nodeHandler(req, res);\n }\n return original(req, res);\n });\n\n console.log(`- Schema server enabled at ${MCP_PATH} (Streamable HTTP)`);\n },\n} satisfies FaapiPlugin;\n","/**\n * 从路由清单生成接口描述信息(含输入参数与响应类型)\n *\n * 直接调用主包 `collectRouteSchemaSources` 执行 AST 分析,提取每个路由 handler 的\n * 输入参数类型与返回类型,无需依赖运行时 schemaRegistry。\n *\n * 响应类型提取独立实现 Promise 解包逻辑,不修改主包 resolveTypeNode\n * (主包在运行时校验场景下遇到 Promise 会抛错,响应类型提取场景需解包取 T)。\n */\n\nimport ts from 'typescript';\nimport {\n collectRouteSchemaSources,\n createPrograms,\n invalidateProgramCache,\n extractTypeInfo,\n resolveTypeNode,\n getInputTypeForMethod,\n SchemaExtractionError,\n type RouteManifest,\n type RouteInfo,\n type RouteInputSchema,\n type RouteOutputSchema,\n type RouteParamSchema,\n type RuntimeType,\n type PropertyType,\n} from '@faapi/faapi';\n\n/**\n * 从路由清单生成接口描述信息\n *\n * @param routes 路由清单\n * @param rootDir 项目根目录(用于解析源文件绝对路径)\n */\nexport function buildRouteSchemas(routes: RouteManifest, rootDir: string): RouteInfo[] {\n const { sources } = collectRouteSchemaSources(routes, rootDir);\n\n // 按 urlPath#schemaName 索引(schemaName = `${method}${inputType}`,如 GETQuery)\n const sourceMap = new Map<string, (typeof sources)[number]>();\n for (const source of sources) {\n sourceMap.set(`${source.urlPath}#${source.schemaName}`, source);\n }\n\n // 按 urlPath 索引源文件绝对路径(用于响应类型提取)\n const filePathMap = new Map<string, string>();\n for (const source of sources) {\n filePathMap.set(source.urlPath, source.filePath);\n }\n\n // 响应类型提取与输入提取共享同一批 Program(createPrograms 内部按 tsconfig 分组缓存)——\n // 此前逐路由单文件 createProgram,每个 Program 都加载全项目源码,与输入提取形成双份解析\n const programByFile = createPrograms([...new Set(filePathMap.values())]);\n\n return routes.map((route) => {\n const inputs = extractInputSchemas(route, sourceMap);\n const filePath = filePathMap.get(route.urlPath);\n const output = extractOutputSchema(\n route,\n filePath,\n filePath ? programByFile.get(filePath) : undefined,\n );\n return {\n method: route.method,\n path: route.urlPath,\n filePath: route.filePath,\n isDynamic: route.isDynamic,\n inputs,\n output,\n };\n });\n}\n\n/**\n * 提取一个路由的所有输入 schema\n *\n * 从 sourceMap 查询 AST 提取结果;无类型声明时 properties 为空。\n * 动态路由无 params 类型声明时,用 paramNames 兜底为 string[]。\n */\nfunction extractInputSchemas(\n route: { method: string; urlPath: string; isDynamic: boolean; paramNames: string[] },\n sourceMap: Map<\n string,\n { schemaName: string; typeInfo: { name: string; properties: PropertyType[] } | null }\n >,\n): RouteInputSchema[] {\n const inputs: RouteInputSchema[] = [];\n\n // 主输入(query 或 body)\n const inputType = getInputTypeForMethod(route.method);\n const schemaName = `${route.method.toUpperCase()}${capitalize(inputType)}`;\n const source = sourceMap.get(`${route.urlPath}#${schemaName}`);\n\n if (source?.typeInfo) {\n inputs.push({\n source: inputType,\n schemaName: source.typeInfo.name,\n properties: toParamSchemas(source.typeInfo.properties),\n });\n } else {\n // 无类型声明\n inputs.push({\n source: inputType,\n schemaName: null,\n properties: [],\n });\n }\n\n // 动态路由参数:collectRouteSchemaSources 不提取 params 类型,用 paramNames 兜底\n if (route.isDynamic && route.paramNames.length > 0) {\n inputs.push({\n source: 'params',\n schemaName: null,\n properties: route.paramNames.map((name) => ({ name, type: 'string', required: true })),\n });\n }\n\n return inputs;\n}\n\n/**\n * 提取路由的响应类型\n *\n * 流程:\n * 1. 用 TypeScript Compiler API 定位 handler 函数节点\n * 2. 读取 node.type(显式返回类型注解)\n * 3. 无注解 → 返回 null\n * 4. 解包 Promise<T>(若存在)→ 解析 T\n * 5. void/Promise<void> → 返回 null\n * 6. 解析失败 catch SchemaExtractionError → 降级返回 null(不阻断整条路由 schema 构建)\n *\n * @param route 路由信息\n * @param filePath 源文件绝对路径(已由 collectRouteSchemaSources 解析)\n */\nfunction extractOutputSchema(\n route: { method: string; urlPath: string },\n filePath: string | undefined,\n program: import('typescript').Program | undefined,\n): RouteOutputSchema | null {\n if (!filePath || !program) return null;\n\n try {\n const sourceFile = program.getSourceFile(filePath);\n if (!sourceFile) return null;\n\n const checker = program.getTypeChecker();\n\n // 定位 handler 函数声明(export function GET/POST/...)\n const handlerName = route.method.toUpperCase();\n let returnTypeNode: ts.TypeNode | undefined;\n ts.forEachChild(sourceFile, (node) => {\n if (returnTypeNode) return;\n if (ts.isFunctionDeclaration(node) && node.name?.text === handlerName) {\n returnTypeNode = node.type;\n }\n });\n\n if (!returnTypeNode) return null;\n\n // 解包 Promise<T>\n const unwrapped = unwrapPromise(returnTypeNode);\n if (unwrapped === null) return null; // void\n\n // 解析解包后的类型\n const runtimeType = resolveTypeNode(unwrapped, checker);\n return runtimeTypeToOutput(unwrapped, runtimeType, program, filePath, checker);\n } catch (err) {\n // 解析失败降级为 null,不阻断整条路由 schema 构建\n if (err instanceof SchemaExtractionError) return null;\n throw err;\n }\n}\n\n/**\n * 解包 Promise<T> → T\n *\n * - Promise<T> → T\n * - Promise<void> → null(表示无响应类型)\n * - 非 Promise 类型 → 原样返回\n *\n * 仅解包一层,不递归(Promise<Promise<T>> 实际不会出现)。\n */\nfunction unwrapPromise(typeNode: ts.TypeNode): ts.TypeNode | null {\n if (!ts.isTypeReferenceNode(typeNode)) return typeNode;\n\n const typeName = typeNode.typeName.getText();\n if (typeName !== 'Promise') return typeNode;\n\n // Promise 无类型参数 → 视为 Promise<any> → 原样返回(any 由 resolveTypeNode 处理)\n const arg = typeNode.typeArguments?.[0];\n if (!arg) return typeNode;\n\n // Promise<void> → null\n if (ts.isTypeNode(arg) && arg.kind === ts.SyntaxKind.VoidKeyword) return null;\n\n return arg;\n}\n\n/**\n * 将 RuntimeType + 原始类型节点转换为 RouteOutputSchema\n *\n * - 命名类型引用(TypeReference)→ 调 extractTypeInfo 提取完整结构 → properties 来自 typeInfo\n * - 数组类型 T[](TypeReference Array<T>)→ 取元素类型名,提取元素结构\n * - 内联对象字面量(TypeLiteralNode)→ 直接从 runtimeType.properties 提取\n * - 其他(基础类型/联合/元组等)→ properties 为空,schemaName 为 null\n */\nfunction runtimeTypeToOutput(\n typeNode: ts.TypeNode,\n runtimeType: RuntimeType,\n program: ts.Program,\n filePath: string,\n _checker: ts.TypeChecker,\n): RouteOutputSchema {\n // 内联对象字面量 → 直接从 runtimeType 提取 properties\n if (ts.isTypeLiteralNode(typeNode)) {\n if (runtimeType.kind === 'object') {\n return {\n schemaName: null,\n properties: toParamSchemas(runtimeType.properties),\n };\n }\n }\n\n // 数组类型 T[](ArrayTypeNode)→ 取元素类型提取结构\n if (ts.isArrayTypeNode(typeNode) && runtimeType.kind === 'array') {\n const element = runtimeType.element;\n // 元素是命名类型引用 → 提取元素结构\n if (element.kind === 'ref') {\n const typeInfo = extractTypeInfo(program, filePath, element.name);\n if (typeInfo) {\n return {\n schemaName: element.name,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n // 元素已被 checker 展开为 object(resolveTypeNode 默认行为)→ 尝试从 AST 节点拿命名\n if (element.kind === 'object') {\n const elementNode = typeNode.elementType;\n if (elementNode && ts.isTypeReferenceNode(elementNode)) {\n const elementName = elementNode.typeName.getText();\n const typeInfo = extractTypeInfo(program, filePath, elementName);\n if (typeInfo) {\n return {\n schemaName: elementName,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n // 内联对象数组({ id: number }[])\n return {\n schemaName: null,\n properties: toParamSchemas(element.properties),\n };\n }\n // 数组元素非命名类型(如 string[]、number[])\n return { schemaName: null, properties: [] };\n }\n\n // 命名类型引用 → 提取完整结构\n if (ts.isTypeReferenceNode(typeNode)) {\n const typeName = typeNode.typeName.getText();\n\n // 数组类型 Array<T>(TypeReference Array<T>)→ 取元素类型名提取结构\n if (runtimeType.kind === 'array') {\n const element = runtimeType.element;\n if (element.kind === 'ref') {\n const typeInfo = extractTypeInfo(program, filePath, element.name);\n if (typeInfo) {\n return {\n schemaName: element.name,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n // 数组元素是内联对象({ id: number }[])\n if (element.kind === 'object') {\n return {\n schemaName: null,\n properties: toParamSchemas(element.properties),\n };\n }\n // 数组元素非命名类型(如 string[]、number[])\n return { schemaName: null, properties: [] };\n }\n\n // 命名类型引用(interface/type alias)\n if (runtimeType.kind === 'ref' || runtimeType.kind === 'object') {\n const typeInfo = extractTypeInfo(program, filePath, typeName);\n if (typeInfo) {\n return {\n schemaName: typeName,\n properties: toParamSchemas(typeInfo.properties),\n };\n }\n }\n }\n\n // 其他类型(基础类型/联合/元组等)\n return { schemaName: null, properties: [] };\n}\n\n/**\n * 首字母大写\n */\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n/**\n * 将 PropertyType[] 转换为 RouteParamSchema[]\n */\nfunction toParamSchemas(properties: PropertyType[]): RouteParamSchema[] {\n return properties.map((prop) => ({\n name: prop.name,\n type: runtimeTypeToString(prop.type),\n required: !prop.optional,\n }));\n}\n\n/**\n * 将 RuntimeType 转为可读字符串\n */\nfunction runtimeTypeToString(type: RuntimeType): string {\n switch (type.kind) {\n case 'string':\n case 'number':\n case 'boolean':\n case 'bigint':\n case 'null':\n case 'undefined':\n case 'date':\n return type.kind;\n case 'literal':\n return JSON.stringify(type.value);\n case 'array':\n return `${runtimeTypeToString(type.element)}[]`;\n case 'tuple':\n return `[${type.elements.map((e) => (e.rest ? '...' : '') + runtimeTypeToString(e.type) + (e.optional ? '?' : '')).join(', ')}]`;\n case 'object':\n return 'object';\n case 'union':\n return type.members.map(runtimeTypeToString).join(' | ');\n case 'record':\n return `Record<${runtimeTypeToString(type.key)}, ${runtimeTypeToString(type.value)}>`;\n case 'ref':\n return type.name;\n case 'any':\n case 'unknown':\n return 'unknown';\n default:\n return 'unknown';\n }\n}\n\n// 显式导出 invalidateProgramCache 便于测试清理缓存\nexport { invalidateProgramCache };\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faapi/schema",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Schema introspection for faapi — expose API schema to AI assistants via MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,20 +19,20 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@types/node": "^
|
|
23
|
-
"@vitest/coverage-v8": "
|
|
22
|
+
"@types/node": "^24.3.0",
|
|
23
|
+
"@vitest/coverage-v8": "4",
|
|
24
24
|
"tsup": "^8.4.0",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
|
-
"vitest": "^
|
|
26
|
+
"vitest": "^4.1.11",
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@faapi/faapi": "
|
|
29
|
-
"@faapi/mcp": "
|
|
28
|
+
"@faapi/faapi": "4.1.0",
|
|
29
|
+
"@faapi/mcp": "4.1.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"typescript": ">=5.7.0",
|
|
33
33
|
"zod": "^4.4.3",
|
|
34
|
-
"@faapi/faapi": "^
|
|
35
|
-
"@faapi/mcp": "^
|
|
34
|
+
"@faapi/faapi": "^4.1.0",
|
|
35
|
+
"@faapi/mcp": "^4.1.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"typescript": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup",
|
|
54
54
|
"test": "vitest run --passWithNoTests",
|
|
55
|
-
"typecheck": "tsc --noEmit"
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"typecheck:test": "tsc --noEmit"
|
|
56
57
|
}
|
|
57
58
|
}
|