@faapi/schema 0.0.0-canary.0e994fe → 0.0.0-canary.22b65a2
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 +60 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,15 +4,18 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
// src/routeSchema.ts
|
|
7
|
-
import path from "path";
|
|
8
7
|
import {
|
|
9
|
-
|
|
8
|
+
collectRouteSchemaSources,
|
|
10
9
|
getInputTypeForMethod
|
|
11
10
|
} from "@faapi/faapi";
|
|
12
11
|
function buildRouteSchemas(routes, rootDir) {
|
|
12
|
+
const { sources } = collectRouteSchemaSources(routes, rootDir);
|
|
13
|
+
const sourceMap = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const source of sources) {
|
|
15
|
+
sourceMap.set(`${source.urlPath}#${source.schemaName}`, source);
|
|
16
|
+
}
|
|
13
17
|
return routes.map((route) => {
|
|
14
|
-
const
|
|
15
|
-
const inputs = extractInputSchemas(absoluteFilePath, route.method, route);
|
|
18
|
+
const inputs = extractInputSchemas(route, sourceMap);
|
|
16
19
|
return {
|
|
17
20
|
method: route.method,
|
|
18
21
|
path: route.urlPath,
|
|
@@ -22,15 +25,16 @@ function buildRouteSchemas(routes, rootDir) {
|
|
|
22
25
|
};
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
|
-
function extractInputSchemas(
|
|
28
|
+
function extractInputSchemas(route, sourceMap) {
|
|
26
29
|
const inputs = [];
|
|
27
|
-
const inputType = getInputTypeForMethod(method);
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
+
const inputType = getInputTypeForMethod(route.method);
|
|
31
|
+
const schemaName = `${route.method.toUpperCase()}${capitalize(inputType)}`;
|
|
32
|
+
const source = sourceMap.get(`${route.urlPath}#${schemaName}`);
|
|
33
|
+
if (source?.typeInfo) {
|
|
30
34
|
inputs.push({
|
|
31
35
|
source: inputType,
|
|
32
|
-
schemaName:
|
|
33
|
-
properties:
|
|
36
|
+
schemaName: source.typeInfo.name,
|
|
37
|
+
properties: toParamSchemas(source.typeInfo.properties)
|
|
34
38
|
});
|
|
35
39
|
} else {
|
|
36
40
|
inputs.push({
|
|
@@ -40,20 +44,55 @@ function extractInputSchemas(filePath, method, route) {
|
|
|
40
44
|
});
|
|
41
45
|
}
|
|
42
46
|
if (route.isDynamic && route.paramNames.length > 0) {
|
|
43
|
-
const paramsSchema = getSchemaProperties(filePath, method, "params");
|
|
44
|
-
const paramsProps = paramsSchema && paramsSchema.schemaName ? paramsSchema.properties : route.paramNames.map((name) => ({
|
|
45
|
-
name,
|
|
46
|
-
type: "string",
|
|
47
|
-
required: true
|
|
48
|
-
}));
|
|
49
47
|
inputs.push({
|
|
50
48
|
source: "params",
|
|
51
|
-
schemaName:
|
|
52
|
-
properties:
|
|
49
|
+
schemaName: null,
|
|
50
|
+
properties: route.paramNames.map((name) => ({ name, type: "string", required: true }))
|
|
53
51
|
});
|
|
54
52
|
}
|
|
55
53
|
return inputs;
|
|
56
54
|
}
|
|
55
|
+
function capitalize(s) {
|
|
56
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
57
|
+
}
|
|
58
|
+
function toParamSchemas(properties) {
|
|
59
|
+
return properties.map((prop) => ({
|
|
60
|
+
name: prop.name,
|
|
61
|
+
type: runtimeTypeToString(prop.type),
|
|
62
|
+
required: !prop.optional
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function runtimeTypeToString(type) {
|
|
66
|
+
switch (type.kind) {
|
|
67
|
+
case "string":
|
|
68
|
+
case "number":
|
|
69
|
+
case "boolean":
|
|
70
|
+
case "bigint":
|
|
71
|
+
case "null":
|
|
72
|
+
case "undefined":
|
|
73
|
+
case "date":
|
|
74
|
+
return type.kind;
|
|
75
|
+
case "literal":
|
|
76
|
+
return JSON.stringify(type.value);
|
|
77
|
+
case "array":
|
|
78
|
+
return `${runtimeTypeToString(type.element)}[]`;
|
|
79
|
+
case "tuple":
|
|
80
|
+
return `[${type.elements.map((e) => (e.rest ? "..." : "") + runtimeTypeToString(e.type) + (e.optional ? "?" : "")).join(", ")}]`;
|
|
81
|
+
case "object":
|
|
82
|
+
return "object";
|
|
83
|
+
case "union":
|
|
84
|
+
return type.members.map(runtimeTypeToString).join(" | ");
|
|
85
|
+
case "record":
|
|
86
|
+
return `Record<${runtimeTypeToString(type.key)}, ${runtimeTypeToString(type.value)}>`;
|
|
87
|
+
case "ref":
|
|
88
|
+
return type.name;
|
|
89
|
+
case "any":
|
|
90
|
+
case "unknown":
|
|
91
|
+
return "unknown";
|
|
92
|
+
default:
|
|
93
|
+
return "unknown";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
57
96
|
|
|
58
97
|
// src/schemaServer.ts
|
|
59
98
|
function isSchemaEnabled() {
|
|
@@ -103,15 +142,15 @@ function createSchemaServer(routes, rootDir) {
|
|
|
103
142
|
method: z.string().describe("HTTP \u65B9\u6CD5\uFF0C\u5982 GET\u3001POST"),
|
|
104
143
|
path: z.string().describe("\u8DEF\u7531\u8DEF\u5F84\uFF0C\u5982 /auth/login")
|
|
105
144
|
},
|
|
106
|
-
({ method, path
|
|
145
|
+
({ method, path }) => {
|
|
107
146
|
const schemas = getSchemas();
|
|
108
|
-
const route = schemas.find((r) => r.method === method.toUpperCase() && r.path ===
|
|
147
|
+
const route = schemas.find((r) => r.method === method.toUpperCase() && r.path === path);
|
|
109
148
|
if (!route) {
|
|
110
149
|
return {
|
|
111
150
|
content: [
|
|
112
151
|
{
|
|
113
152
|
type: "text",
|
|
114
|
-
text: JSON.stringify({ error: `\u672A\u627E\u5230\u8DEF\u7531 ${method.toUpperCase()} ${
|
|
153
|
+
text: JSON.stringify({ error: `\u672A\u627E\u5230\u8DEF\u7531 ${method.toUpperCase()} ${path}` })
|
|
115
154
|
}
|
|
116
155
|
]
|
|
117
156
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemaServer.ts","../src/routeSchema.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport type { RouteManifest, RouteInfo, FaapiPlugin, PluginContext } from '@faapi/faapi';\nimport { buildRouteSchemas } from './routeSchema';\n\n/**\n * 判断 schema server 是否应该启用\n * - FAAPI_SCHEMA=1 强制开启\n * - FAAPI_SCHEMA=0 强制关闭\n * - 未设置时:开发环境默认开启,生产环境默认关闭\n */\nexport function isSchemaEnabled(): boolean {\n const envValue = process.env.FAAPI_SCHEMA;\n if (envValue === '1' || envValue === 'true') return true;\n if (envValue === '0' || envValue === 'false') return false;\n // 未设置时根据 NODE_ENV 判断\n return process.env.NODE_ENV !== 'production';\n}\n\n/**\n * 创建 faapi Schema Server\n * 通过 MCP 协议暴露路由信息供 LLM 查询\n */\nexport function createSchemaServer(routes: RouteManifest, rootDir: string): McpServer {\n const server = new McpServer({\n name: 'faapi-schema',\n version: '0.0.1',\n });\n\n // 缓存 route schemas\n let cachedSchemas: RouteInfo[] | null = null;\n\n function getSchemas(): RouteInfo[] {\n if (!cachedSchemas) {\n cachedSchemas = buildRouteSchemas(routes, rootDir);\n }\n return cachedSchemas;\n }\n\n // Tool: 列出所有路由\n server.tool(\n 'list_routes',\n '列出当前 faapi 应用的所有 API 路由,包括方法、路径、是否动态路由',\n {},\n () => {\n const schemas = getSchemas();\n const routesList = schemas.map((r) => ({\n method: r.method,\n path: r.path,\n isDynamic: r.isDynamic,\n filePath: r.filePath,\n }));\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(routesList, null, 2),\n },\n ],\n };\n },\n );\n\n // Tool: 获取单个路由的详细 schema\n server.tool(\n 'get_route_schema',\n '获取指定路由的详细接口信息,包括输入参数的名称、类型、是否必填',\n {\n method: z.string().describe('HTTP 方法,如 GET、POST'),\n path: z.string().describe('路由路径,如 /auth/login'),\n },\n ({ method, path }) => {\n const schemas = getSchemas();\n const route = schemas.find((r) => r.method === method.toUpperCase() && r.path === path);\n\n if (!route) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: `未找到路由 ${method.toUpperCase()} ${path}` }),\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(route, null, 2),\n },\n ],\n };\n },\n );\n\n // Tool: 获取所有路由的完整 schema(类似 OpenAPI)\n server.tool(\n 'get_api_schema',\n '获取当前应用所有接口的完整 schema,类似 OpenAPI 规范,包含每个路由的输入参数定义',\n {},\n () => {\n const schemas = getSchemas();\n const apiSchema: Record<string, unknown> = {};\n\n for (const route of schemas) {\n const key = `${route.method} ${route.path}`;\n apiSchema[key] = {\n method: route.method,\n path: route.path,\n isDynamic: route.isDynamic,\n inputs: route.inputs.map((input) => ({\n source: input.source,\n schemaName: input.schemaName,\n properties: input.properties,\n })),\n };\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(apiSchema, null, 2),\n },\n ],\n };\n },\n );\n\n return server;\n}\n\n/**\n * 启动 Schema Server(stdio 模式)\n */\nexport async function startSchemaServer(routes: RouteManifest, rootDir: string): Promise<void> {\n const server = createSchemaServer(routes, rootDir);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\n/**\n * faapi 插件入口\n *\n * 在 faapi.config.ts 中声明:\n * ```ts\n * export default {\n * plugins: ['@faapi/schema'],\n * } satisfies FaapiConfig;\n * ```\n */\nexport default {\n name: '@faapi/schema',\n setup(ctx: PluginContext) {\n if (!isSchemaEnabled()) {\n console.log('- Schema server disabled (FAAPI_SCHEMA=0 or production mode)');\n return;\n }\n console.log('- Schema server enabled (stdio)');\n // startSchemaServer 是异步的,但不阻塞启动流程\n void startSchemaServer(ctx.routes, ctx.rootDir);\n },\n} satisfies FaapiPlugin;\n","import path from 'node:path';\nimport {\n getSchemaProperties,\n getInputTypeForMethod,\n type RouteManifest,\n type RouteInfo,\n type RouteInputSchema,\n} from '@faapi/faapi';\n\n/**\n * 从路由清单生成接口描述信息\n *\n * 复用主包 schemaRegistry 已有的类型提取结果,避免重复 AST 分析。\n *\n * @param routes 路由清单\n * @param rootDir 根目录\n */\nexport function buildRouteSchemas(routes: RouteManifest, rootDir: string): RouteInfo[] {\n return routes.map((route) => {\n const absoluteFilePath = path.resolve(rootDir, route.filePath);\n const inputs = extractInputSchemas(absoluteFilePath, route.method, route);\n\n return {\n method: route.method,\n path: route.urlPath,\n filePath: route.filePath,\n isDynamic: route.isDynamic,\n inputs,\n };\n });\n}\n\n/**\n * 提取一个路由文件的所有输入 schema\n *\n * 直接查询 schemaRegistry,复用参数校验已提取的 PropertyType,\n * 不再重复执行 AST 分析。\n */\nfunction extractInputSchemas(\n filePath: string,\n method: string,\n route: { isDynamic: boolean; paramNames: string[] },\n): RouteInputSchema[] {\n const inputs: RouteInputSchema[] = [];\n\n // 主输入(query 或 body):从 registry 查询\n const inputType = getInputTypeForMethod(method);\n const schema = getSchemaProperties(filePath, method, inputType);\n\n if (schema) {\n inputs.push({\n source: inputType,\n schemaName: schema.schemaName,\n properties: schema.properties,\n });\n } else {\n // registry 无数据(不应发生,插件 setup 时 registry 已加载)\n inputs.push({\n source: inputType,\n schemaName: null,\n properties: [],\n });\n }\n\n // 动态路由参数\n if (route.isDynamic && route.paramNames.length > 0) {\n const paramsSchema = getSchemaProperties(filePath, method, 'params');\n\n // params 有类型声明时用类型信息,否则用 paramNames 兜底\n const paramsProps =\n paramsSchema && paramsSchema.schemaName\n ? paramsSchema.properties\n : route.paramNames.map((name) => ({\n name,\n type: 'string',\n required: true,\n }));\n\n inputs.push({\n source: 'params',\n schemaName: paramsSchema?.schemaName ?? null,\n properties: paramsProps,\n });\n }\n\n return inputs;\n}\n"],"mappings":";AAAA,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,SAAS;;;ACFlB,OAAO,UAAU;AACjB;AAAA,EACE;AAAA,EACA;AAAA,OAIK;AAUA,SAAS,kBAAkB,QAAuB,SAA8B;AACrF,SAAO,OAAO,IAAI,CAAC,UAAU;AAC3B,UAAM,mBAAmB,KAAK,QAAQ,SAAS,MAAM,QAAQ;AAC7D,UAAM,SAAS,oBAAoB,kBAAkB,MAAM,QAAQ,KAAK;AAExE,WAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM,MAAM;AAAA,MACZ,UAAU,MAAM;AAAA,MAChB,WAAW,MAAM;AAAA,MACjB;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAQA,SAAS,oBACP,UACA,QACA,OACoB;AACpB,QAAM,SAA6B,CAAC;AAGpC,QAAM,YAAY,sBAAsB,MAAM;AAC9C,QAAM,SAAS,oBAAoB,UAAU,QAAQ,SAAS;AAE9D,MAAI,QAAQ;AACV,WAAO,KAAK;AAAA,MACV,QAAQ;AAAA,MACR,YAAY,OAAO;AAAA,MACnB,YAAY,OAAO;AAAA,IACrB,CAAC;AAAA,EACH,OAAO;AAEL,WAAO,KAAK;AAAA,MACV,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,YAAY,CAAC;AAAA,IACf,CAAC;AAAA,EACH;AAGA,MAAI,MAAM,aAAa,MAAM,WAAW,SAAS,GAAG;AAClD,UAAM,eAAe,oBAAoB,UAAU,QAAQ,QAAQ;AAGnE,UAAM,cACJ,gBAAgB,aAAa,aACzB,aAAa,aACb,MAAM,WAAW,IAAI,CAAC,UAAU;AAAA,MAC9B;AAAA,MACA,MAAM;AAAA,MACN,UAAU;AAAA,IACZ,EAAE;AAER,WAAO,KAAK;AAAA,MACV,QAAQ;AAAA,MACR,YAAY,cAAc,cAAc;AAAA,MACxC,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;AD1EO,SAAS,kBAA2B;AACzC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,aAAa,OAAO,aAAa,OAAQ,QAAO;AACpD,MAAI,aAAa,OAAO,aAAa,QAAS,QAAO;AAErD,SAAO,QAAQ,IAAI,aAAa;AAClC;AAMO,SAAS,mBAAmB,QAAuB,SAA4B;AACpF,QAAM,SAAS,IAAI,UAAU;AAAA,IAC3B,MAAM;AAAA,IACN,SAAS;AAAA,EACX,CAAC;AAGD,MAAI,gBAAoC;AAExC,WAAS,aAA0B;AACjC,QAAI,CAAC,eAAe;AAClB,sBAAgB,kBAAkB,QAAQ,OAAO;AAAA,IACnD;AACA,WAAO;AAAA,EACT;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,MAAM;AACJ,YAAM,UAAU,WAAW;AAC3B,YAAM,aAAa,QAAQ,IAAI,CAAC,OAAO;AAAA,QACrC,QAAQ,EAAE;AAAA,QACV,MAAM,EAAE;AAAA,QACR,WAAW,EAAE;AAAA,QACb,UAAU,EAAE;AAAA,MACd,EAAE;AACF,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU,YAAY,MAAM,CAAC;AAAA,UAC1C;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ,EAAE,OAAO,EAAE,SAAS,6CAAoB;AAAA,MAChD,MAAM,EAAE,OAAO,EAAE,SAAS,kDAAoB;AAAA,IAChD;AAAA,IACA,CAAC,EAAE,QAAQ,MAAAA,MAAK,MAAM;AACpB,YAAM,UAAU,WAAW;AAC3B,YAAM,QAAQ,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,OAAO,YAAY,KAAK,EAAE,SAASA,KAAI;AAEtF,UAAI,CAAC,OAAO;AACV,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MAAM,KAAK,UAAU,EAAE,OAAO,kCAAS,OAAO,YAAY,CAAC,IAAIA,KAAI,GAAG,CAAC;AAAA,YACzE;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC;AAAA,UACrC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,MAAM;AACJ,YAAM,UAAU,WAAW;AAC3B,YAAM,YAAqC,CAAC;AAE5C,iBAAW,SAAS,SAAS;AAC3B,cAAM,MAAM,GAAG,MAAM,MAAM,IAAI,MAAM,IAAI;AACzC,kBAAU,GAAG,IAAI;AAAA,UACf,QAAQ,MAAM;AAAA,UACd,MAAM,MAAM;AAAA,UACZ,WAAW,MAAM;AAAA,UACjB,QAAQ,MAAM,OAAO,IAAI,CAAC,WAAW;AAAA,YACnC,QAAQ,MAAM;AAAA,YACd,YAAY,MAAM;AAAA,YAClB,YAAY,MAAM;AAAA,UACpB,EAAE;AAAA,QACJ;AAAA,MACF;AAEA,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU,WAAW,MAAM,CAAC;AAAA,UACzC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAKA,eAAsB,kBAAkB,QAAuB,SAAgC;AAC7F,QAAM,SAAS,mBAAmB,QAAQ,OAAO;AACjD,QAAM,YAAY,IAAI,qBAAqB;AAC3C,QAAM,OAAO,QAAQ,SAAS;AAChC;AAYA,IAAO,uBAAQ;AAAA,EACb,MAAM;AAAA,EACN,MAAM,KAAoB;AACxB,QAAI,CAAC,gBAAgB,GAAG;AACtB,cAAQ,IAAI,8DAA8D;AAC1E;AAAA,IACF;AACA,YAAQ,IAAI,iCAAiC;AAE7C,SAAK,kBAAkB,IAAI,QAAQ,IAAI,OAAO;AAAA,EAChD;AACF;","names":["path"]}
|
|
1
|
+
{"version":3,"sources":["../src/schemaServer.ts","../src/routeSchema.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport type { RouteManifest, RouteInfo, FaapiPlugin, PluginContext } from '@faapi/faapi';\nimport { buildRouteSchemas } from './routeSchema';\n\n/**\n * 判断 schema server 是否应该启用\n * - FAAPI_SCHEMA=1 强制开启\n * - FAAPI_SCHEMA=0 强制关闭\n * - 未设置时:开发环境默认开启,生产环境默认关闭\n */\nexport function isSchemaEnabled(): boolean {\n const envValue = process.env.FAAPI_SCHEMA;\n if (envValue === '1' || envValue === 'true') return true;\n if (envValue === '0' || envValue === 'false') return false;\n // 未设置时根据 NODE_ENV 判断\n return process.env.NODE_ENV !== 'production';\n}\n\n/**\n * 创建 faapi Schema Server\n * 通过 MCP 协议暴露路由信息供 LLM 查询\n *\n * @param routes 路由清单\n * @param rootDir 项目根目录(用于 AST 分析解析源文件)\n */\nexport function createSchemaServer(routes: RouteManifest, rootDir: string): McpServer {\n const server = new McpServer({\n name: 'faapi-schema',\n version: '0.0.1',\n });\n\n // 缓存 route schemas\n let cachedSchemas: RouteInfo[] | null = null;\n\n function getSchemas(): RouteInfo[] {\n if (!cachedSchemas) {\n cachedSchemas = buildRouteSchemas(routes, rootDir);\n }\n return cachedSchemas;\n }\n\n // Tool: 列出所有路由\n server.tool(\n 'list_routes',\n '列出当前 faapi 应用的所有 API 路由,包括方法、路径、是否动态路由',\n {},\n () => {\n const schemas = getSchemas();\n const routesList = schemas.map((r) => ({\n method: r.method,\n path: r.path,\n isDynamic: r.isDynamic,\n filePath: r.filePath,\n }));\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(routesList, null, 2),\n },\n ],\n };\n },\n );\n\n // Tool: 获取单个路由的详细 schema\n server.tool(\n 'get_route_schema',\n '获取指定路由的详细接口信息,包括输入参数的名称、类型、是否必填',\n {\n method: z.string().describe('HTTP 方法,如 GET、POST'),\n path: z.string().describe('路由路径,如 /auth/login'),\n },\n ({ method, path }) => {\n const schemas = getSchemas();\n const route = schemas.find((r) => r.method === method.toUpperCase() && r.path === path);\n\n if (!route) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: `未找到路由 ${method.toUpperCase()} ${path}` }),\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(route, null, 2),\n },\n ],\n };\n },\n );\n\n // Tool: 获取所有路由的完整 schema(类似 OpenAPI)\n server.tool(\n 'get_api_schema',\n '获取当前应用所有接口的完整 schema,类似 OpenAPI 规范,包含每个路由的输入参数定义',\n {},\n () => {\n const schemas = getSchemas();\n const apiSchema: Record<string, unknown> = {};\n\n for (const route of schemas) {\n const key = `${route.method} ${route.path}`;\n apiSchema[key] = {\n method: route.method,\n path: route.path,\n isDynamic: route.isDynamic,\n inputs: route.inputs.map((input) => ({\n source: input.source,\n schemaName: input.schemaName,\n properties: input.properties,\n })),\n };\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(apiSchema, null, 2),\n },\n ],\n };\n },\n );\n\n return server;\n}\n\n/**\n * 启动 Schema Server(stdio 模式)\n *\n * @param routes 路由清单\n * @param rootDir 项目根目录(用于 AST 分析解析源文件)\n */\nexport async function startSchemaServer(routes: RouteManifest, rootDir: string): Promise<void> {\n const server = createSchemaServer(routes, rootDir);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\n/**\n * faapi 插件入口\n *\n * 在 faapi.config.ts 中声明:\n * ```ts\n * export default {\n * plugins: ['@faapi/schema'],\n * } satisfies FaapiConfig;\n * ```\n */\nexport default {\n name: '@faapi/schema',\n setup(ctx: PluginContext) {\n if (!isSchemaEnabled()) {\n console.log('- Schema server disabled (FAAPI_SCHEMA=0 or production mode)');\n return;\n }\n console.log('- Schema server enabled (stdio)');\n // startSchemaServer 是异步的,但不阻塞启动流程\n void startSchemaServer(ctx.routes, ctx.rootDir);\n },\n} satisfies FaapiPlugin;\n","import {\n collectRouteSchemaSources,\n getInputTypeForMethod,\n type RouteManifest,\n type RouteInfo,\n type RouteInputSchema,\n type RouteParamSchema,\n type RuntimeType,\n type PropertyType,\n} from '@faapi/faapi';\n\n/**\n * 从路由清单生成接口描述信息\n *\n * 直接调用主包 `collectRouteSchemaSources` 执行 AST 分析,提取每个路由 handler 的\n * 输入参数类型,无需依赖运行时 schemaRegistry。\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 return routes.map((route) => {\n const inputs = extractInputSchemas(route, sourceMap);\n return {\n method: route.method,\n path: route.urlPath,\n filePath: route.filePath,\n isDynamic: route.isDynamic,\n inputs,\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 */\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"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faapi/schema",
|
|
3
|
-
"version": "0.0.0-canary.
|
|
3
|
+
"version": "0.0.0-canary.22b65a2",
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
22
|
"zod": "^4.4.3",
|
|
23
|
-
"@faapi/faapi": "0.0.0-canary.
|
|
23
|
+
"@faapi/faapi": "0.0.0-canary.22b65a2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^22.15.0",
|