@blod-nm/nuxt-mcp 0.0.0-stage → 0.1.1
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/README.md +80 -2
- package/package.json +29 -4
- package/runtime/server/api/mcp.post.js +166 -0
- package/runtime/server/utils/mcp.js +176 -0
- package/src/module.js +37 -0
package/README.md
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blod-nm/nuxt-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
按 **MCP 2026-07-28**(Streamable HTTP,**不走 SSE**)把能力暴露成唯一 HTTP 出口。模块零能力,能力由外部注册。纯 JS。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// nuxt.config.ts
|
|
9
|
+
export default defineNuxtConfig({ modules: ['@blod-nm/nuxt-mcp'] }) // 出口:POST /api/mcp
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// server/plugins/userkit.js —— 按大类注册(名字扁平,无 namespace)
|
|
14
|
+
export default defineNitroPlugin(() => {
|
|
15
|
+
registerMcpTools(() => [{
|
|
16
|
+
name: 'user_me',
|
|
17
|
+
title: '获取当前用户',
|
|
18
|
+
description: '返回当前登录用户资料',
|
|
19
|
+
inputSchema: { type: 'object', properties: { access_token: { type: 'string' } }, required: ['access_token'] },
|
|
20
|
+
annotations: { readOnlyHint: true },
|
|
21
|
+
run: args => kit.ctx.run('user.me', args),
|
|
22
|
+
}])
|
|
23
|
+
|
|
24
|
+
registerMcpResources(() => [{
|
|
25
|
+
uri: 'userkit://readme', name: 'readme', mimeType: 'text/markdown',
|
|
26
|
+
read: async uri => ({ contents: [{ uri, mimeType: 'text/markdown', text: '# hi' }] }),
|
|
27
|
+
}])
|
|
28
|
+
|
|
29
|
+
registerMcpPrompts(() => [{
|
|
30
|
+
name: 'greet', description: '打招呼', arguments: [{ name: 'who', required: true }],
|
|
31
|
+
render: args => ({ messages: [{ role: 'user', content: { type: 'text', text: 'hi ' + args.who } }] }),
|
|
32
|
+
}])
|
|
33
|
+
|
|
34
|
+
registerMcpCompletion(async ({ argument }) => ['张三', '李四'].filter(v => v.startsWith(argument.value)))
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 接口(HTTP MCP 全集)
|
|
39
|
+
|
|
40
|
+
| 方法 | 状态 | 说明 |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| `server/discover` | ✅ | `{ supportedVersions, capabilities, instructions, ttlMs, cacheScope }`(取代 initialize) |
|
|
43
|
+
| `tools/list` · `tools/call` | ✅ | 未注册工具 → 空列表 / `-32602` |
|
|
44
|
+
| `resources/list` · `resources/templates/list` · `resources/read` | ✅ | 未注册资源 → 空列表 / `-32602` |
|
|
45
|
+
| `prompts/list` · `prompts/get` | ✅ | 未注册提示词 → 空列表 / `-32602` |
|
|
46
|
+
| `completion/complete` | ✅ | 未注册补全器 → `-32601` |
|
|
47
|
+
| `resources/subscribe` · `resources/unsubscribe` · `subscriptions/listen` | ⛔ 预留 | 需要 SSE 流 → `-32601` 并说明原因 |
|
|
48
|
+
| `notifications/*` | ✅ | `202 Accepted`,无 body |
|
|
49
|
+
| `initialize` · `ping` · `logging/setLevel` | ✅ 兼容层 | 2026 已移除,但为旧客户端保留:initialize 协商版本、ping 回 `{}`、setLevel 接受(无 SSE 不推送) |
|
|
50
|
+
|
|
51
|
+
## 协议要点
|
|
52
|
+
|
|
53
|
+
- 每个 result 都带 `resultType: "complete"`;列表/读取类带 `ttlMs`、`cacheScope`
|
|
54
|
+
- 版本协商:`_meta["io.modelcontextprotocol/protocolVersion"]` 与 `MCP-Protocol-Version` 头不一致 → `400` + `-32020`;版本不支持 → `-32022`
|
|
55
|
+
- 名字:MCP 只允许 `[A-Za-z0-9_-]`,非法字符注册时替换为 `_`;**重名注册直接报错**
|
|
56
|
+
- `tools/call`:`structuredContent` 可为任意 JSON;工具自身错误 → `isError:true`;找不到工具 → `-32602`
|
|
57
|
+
- 传输:`GET → 405`;Origin 跨域 → `403`;Accept 不含 json/event-stream → `406`
|
|
58
|
+
|
|
59
|
+
## 版本兼容
|
|
60
|
+
|
|
61
|
+
| 客户端版本 | 握手方式 | 支持 |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| 2026-07-28 | `server/discover` + 每请求 `_meta` | ✅ |
|
|
64
|
+
| 2025-11-25 / 2025-06-18 / 2025-03-26 | `initialize`(协商到对应版本) | ✅ 兼容层 |
|
|
65
|
+
| 2024-11-05 | `initialize` | ⚠️ 未列入白名单(建议升级) |
|
|
66
|
+
|
|
67
|
+
- 兼容层只加不改:新版继续走 `server/discover`;旧版走 `initialize`,两套共用同一份注册表与结果构造。
|
|
68
|
+
- `initialize` 的结果不带 `resultType`(旧客户端不认这个字段);其余请求的结果始终带。
|
|
69
|
+
- 旧客户端不带 `_meta` 也能调用;版本校验只在客户端显式声明版本时生效。
|
|
70
|
+
|
|
71
|
+
## 选项
|
|
72
|
+
|
|
73
|
+
| 选项 | 默认 | 说明 |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `route` | `/api/mcp` | 唯一出口路径 |
|
|
76
|
+
|
|
77
|
+
## 约定
|
|
78
|
+
|
|
79
|
+
- 每个大类一次注册 = 整组替换(不是累加);重名在注册时 fail-fast。
|
|
80
|
+
- 能力惰性解析:`*/list`、`*/call` 时才实例化。
|
|
81
|
+
- 不声明 `logging` / `resources.subscribe`:它们需要服务端主动推送,本实现没有 SSE。
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blod-nm/nuxt-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "MCP protocol pipe for Nuxt: one endpoint (initialize/tools/list/tools/call), zero abilities, sources injected by other modules.",
|
|
7
|
+
"main": "./src/module.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/module.js",
|
|
10
|
+
"./runtime/*": "./runtime/*"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@blod-nm/base-ctx": "^0.1.1",
|
|
14
|
+
"@nuxt/kit": "^4.5.2"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"h3": "^1.15.11"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "bun test test/",
|
|
21
|
+
"test:e2e": "bun test e2e/"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"runtime",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"h3": ">=1.15.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
mcp.post.js — 唯一能力出口(应用型 · MCP 2026-07-28 · Streamable HTTP 单端点 · 纯 JS)
|
|
3
|
+
|
|
4
|
+
职责: 按最新协议处理 POST /api/mcp:
|
|
5
|
+
· request(有 id) → 200 + application/json;每个 result 都带 resultType:"complete"
|
|
6
|
+
· notification(无 id) → 202 Accepted,无 body
|
|
7
|
+
· GET → 405(不提供 SSE 流)
|
|
8
|
+
版本协商: server/discover(MUST)+ 每请求 _meta["io.modelcontextprotocol/protocolVersion"],
|
|
9
|
+
与 MCP-Protocol-Version 头不一致 → 400 / -32020;版本不支持 → -32022。
|
|
10
|
+
|
|
11
|
+
位置: @feature/nuxt-mcp → src/module.js addServerHandler → 本文件
|
|
12
|
+
|
|
13
|
+
数据流:
|
|
14
|
+
body { jsonrpc, id, method, params }
|
|
15
|
+
→ server/discover → { supportedVersions, capabilities, instructions, ttlMs, cacheScope }
|
|
16
|
+
→ tools/list / tools/call → Tool[] / { content, structuredContent?, isError }
|
|
17
|
+
→ resources/list|templates|read→ Resource[] / ResourceTemplate[] / { contents }
|
|
18
|
+
→ prompts/list|get → Prompt[] / { description?, messages }
|
|
19
|
+
→ completion/complete → { completion: { values } }
|
|
20
|
+
→ notifications/* → 202
|
|
21
|
+
→ 其余 → -32601
|
|
22
|
+
*/
|
|
23
|
+
import { defineEventHandler, readBody, setResponseStatus, getHeader, setHeader, getRequestURL } from 'h3'
|
|
24
|
+
import {
|
|
25
|
+
callTool, listTools, listResources, listResourceTemplates, readResource,
|
|
26
|
+
listPrompts, getPrompt, complete, mcpCapabilities,
|
|
27
|
+
} from '../utils/mcp'
|
|
28
|
+
|
|
29
|
+
const PROTOCOL_VERSION = '2026-07-28'
|
|
30
|
+
/** 兼容层:旧版客户端走 initialize;新版走 server/discover。都落在这张白名单里 */
|
|
31
|
+
const SUPPORTED_VERSIONS = ['2026-07-28', '2025-11-25', '2025-06-18', '2025-03-26']
|
|
32
|
+
const pickVersion = client => (SUPPORTED_VERSIONS.includes(client) ? client : PROTOCOL_VERSION)
|
|
33
|
+
const HEADER_MISMATCH = -32020
|
|
34
|
+
const UNSUPPORTED_PROTOCOL_VERSION = -32022
|
|
35
|
+
const SERVER_INFO = { name: 'nuxt-mcp', title: 'Nuxt MCP', version: '0.1.0' }
|
|
36
|
+
const INSTRUCTIONS = '先 server/discover 拿能力,再 tools/list 或 prompts/list;tools/call 的 name 形如 <ns>__<tool>。业务错误看 isError。'
|
|
37
|
+
|
|
38
|
+
const META_VERSION = 'io.modelcontextprotocol/protocolVersion'
|
|
39
|
+
/** 依赖服务端推送的接口:只在 SSE 模式下可用 */
|
|
40
|
+
const SSE_METHODS = new Set(['resources/subscribe', 'resources/unsubscribe', 'subscriptions/listen'])
|
|
41
|
+
/** 每个结果都必须带 resultType;可缓存结果再带 ttlMs / cacheScope */
|
|
42
|
+
const plain = (id, payload) => ({ jsonrpc: '2.0', id: id ?? null, result: payload })
|
|
43
|
+
const ok = (id, payload = {}) => ({ jsonrpc: '2.0', id: id ?? null, result: { resultType: 'complete', ...payload } })
|
|
44
|
+
const cacheable = (id, payload, scope = 'public') => ok(id, { ttlMs: 0, cacheScope: scope, ...payload })
|
|
45
|
+
const fail = (id, code, message) => ({ jsonrpc: '2.0', id: id ?? null, error: { code, message } })
|
|
46
|
+
|
|
47
|
+
/** 工具结果:结构化内容可以是任意 JSON 值(2026-07-28 起放宽到 array/标量) */
|
|
48
|
+
const toolResult = out => ({
|
|
49
|
+
content: [{ type: 'text', text: JSON.stringify(out ?? null) }],
|
|
50
|
+
...(out === undefined ? {} : { structuredContent: out }),
|
|
51
|
+
isError: false,
|
|
52
|
+
})
|
|
53
|
+
/** 工具自身错误 → isError;协议错误(找不到工具)另走 JSON-RPC error */
|
|
54
|
+
const toolError = e => ({
|
|
55
|
+
content: [{ type: 'text', text: JSON.stringify({ message: e?.message ?? String(e), code: e?.code, payload: e?.payload }) }],
|
|
56
|
+
isError: true,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export default defineEventHandler(async (event) => {
|
|
60
|
+
const method = event.method?.toUpperCase()
|
|
61
|
+
if (method === 'GET') { setResponseStatus(event, 405); return '' } // 不提供服务端 SSE 流
|
|
62
|
+
|
|
63
|
+
// 规范 MUST:校验 Origin,防 DNS rebinding;同源(或无 Origin,如服务端调用)放行
|
|
64
|
+
const origin = getHeader(event, 'origin')
|
|
65
|
+
const host = getHeader(event, 'host') || getRequestURL(event).host
|
|
66
|
+
if (origin && host) {
|
|
67
|
+
let sameOrigin = false
|
|
68
|
+
try { sameOrigin = new URL(origin).host === host } catch {}
|
|
69
|
+
if (!sameOrigin) { setResponseStatus(event, 403); return '' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Streamable HTTP:客户端声明可接受的内容类型;两个都不接受 → 406
|
|
73
|
+
const accept = String(getHeader(event, 'accept') ?? '')
|
|
74
|
+
if (accept && !accept.includes('*/*') && !accept.includes('application/json') && !accept.includes('text/event-stream')) { // */* 是 curl/fetch 默认值,按通配放行
|
|
75
|
+
setResponseStatus(event, 406)
|
|
76
|
+
return fail(null, -32600, 'Accept 需要 application/json 或 text/event-stream')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const body = await readBody(event).catch(() => null)
|
|
80
|
+
if (!body || typeof body !== 'object' || typeof body.method !== 'string') {
|
|
81
|
+
setResponseStatus(event, 400)
|
|
82
|
+
return fail(null, -32600, 'Invalid Request')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { id, method: rpcMethod, params } = body
|
|
86
|
+
const isNotification = id === undefined || id === null
|
|
87
|
+
|
|
88
|
+
// 通知:接受即 202 无 body(notifications/initialized 已废除,其余通知照旧)
|
|
89
|
+
if (isNotification) {
|
|
90
|
+
if (rpcMethod.startsWith('notifications/')) { setResponseStatus(event, 202); return '' } // h3: 返回 undefined 会被当成 404
|
|
91
|
+
setResponseStatus(event, 400)
|
|
92
|
+
return fail(null, -32600, '通知缺少 id,但不是 notifications/*')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 版本协商(2026-07-28):_meta 里的版本必须与 MCP-Protocol-Version 头一致
|
|
96
|
+
const metaVersion = params?._meta?.[META_VERSION]
|
|
97
|
+
const headerVersion = getHeader(event, 'mcp-protocol-version')
|
|
98
|
+
if (metaVersion && headerVersion && metaVersion !== headerVersion) {
|
|
99
|
+
setResponseStatus(event, 400)
|
|
100
|
+
return fail(id, HEADER_MISMATCH, `版本头与 body 不一致: ${headerVersion} ≠ ${metaVersion}`)
|
|
101
|
+
}
|
|
102
|
+
const requested = metaVersion ?? headerVersion
|
|
103
|
+
if (requested && !SUPPORTED_VERSIONS.includes(requested))
|
|
104
|
+
return fail(id, UNSUPPORTED_PROTOCOL_VERSION, `不支持的协议版本: ${requested}(支持: ${SUPPORTED_VERSIONS.join(', ')})`)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
setHeader(event, 'Content-Type', 'application/json')
|
|
108
|
+
|
|
109
|
+
// —— 兼容层(旧版客户端)——
|
|
110
|
+
// initialize:返回双方都支持的版本;initialize 结果本身不带 resultType(旧客户端不认)
|
|
111
|
+
if (rpcMethod === 'initialize') {
|
|
112
|
+
const v = pickVersion(params?.protocolVersion)
|
|
113
|
+
return plain(id, { protocolVersion: v, capabilities: mcpCapabilities(), serverInfo: SERVER_INFO, instructions: INSTRUCTIONS })
|
|
114
|
+
}
|
|
115
|
+
if (rpcMethod === 'ping') return plain(id, {}) // 2026 已移除,旧客户端探活用
|
|
116
|
+
if (rpcMethod === 'logging/setLevel') return plain(id, {}) // 无 SSE 不推送日志,但接受调用
|
|
117
|
+
|
|
118
|
+
if (rpcMethod === 'server/discover')
|
|
119
|
+
return cacheable(id, { supportedVersions: SUPPORTED_VERSIONS, capabilities: mcpCapabilities(), instructions: INSTRUCTIONS, serverInfo: SERVER_INFO })
|
|
120
|
+
|
|
121
|
+
if (rpcMethod === 'tools/list') return cacheable(id, { tools: listTools() }, 'private')
|
|
122
|
+
if (rpcMethod === 'tools/call') {
|
|
123
|
+
if (typeof params?.name !== 'string' || !params.name) return fail(id, -32602, 'Invalid params: name 必填')
|
|
124
|
+
try {
|
|
125
|
+
return ok(id, toolResult(await callTool(params.name, params.arguments ?? {})))
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e?.rpcCode) return fail(id, e.rpcCode, e.message) // 找不到工具 = 协议错误
|
|
128
|
+
return ok(id, toolError(e))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (rpcMethod === 'resources/list') return cacheable(id, { resources: listResources() }, 'private')
|
|
133
|
+
if (rpcMethod === 'resources/templates/list') return cacheable(id, { resourceTemplates: listResourceTemplates() }, 'private')
|
|
134
|
+
if (rpcMethod === 'resources/read') {
|
|
135
|
+
if (typeof params?.uri !== 'string' || !params.uri) return fail(id, -32602, 'Invalid params: uri 必填')
|
|
136
|
+
try {
|
|
137
|
+
return cacheable(id, await readResource(params.uri), 'private')
|
|
138
|
+
} catch (e) {
|
|
139
|
+
return fail(id, e?.rpcCode ?? -32603, e?.message ?? String(e))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (rpcMethod === 'prompts/list') return cacheable(id, { prompts: listPrompts() }, 'private')
|
|
144
|
+
if (rpcMethod === 'prompts/get') {
|
|
145
|
+
if (typeof params?.name !== 'string' || !params.name) return fail(id, -32602, 'Invalid params: name 必填')
|
|
146
|
+
try {
|
|
147
|
+
return ok(id, await getPrompt(params.name, params.arguments ?? {}))
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return fail(id, e?.rpcCode ?? -32603, e?.message ?? String(e))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 需要 SSE 流的接口:位置预留,明确报不支持(本服务是 POST 单响应传输)
|
|
154
|
+
if (SSE_METHODS.has(rpcMethod))
|
|
155
|
+
return fail(id, -32601, `本服务不提供 SSE 流,不支持: ${rpcMethod}`)
|
|
156
|
+
|
|
157
|
+
if (rpcMethod === 'completion/complete') {
|
|
158
|
+
try {
|
|
159
|
+
return ok(id, await complete(params ?? {}))
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return fail(id, e?.rpcCode ?? -32603, e?.message ?? String(e))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return fail(id, -32601, `Method not found: ${rpcMethod}`)
|
|
166
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
mcp.js — MCP 能力注册表(能力型 · 协议层自己的 ctx · 纯 JS)
|
|
3
|
+
|
|
4
|
+
职责: 按 MCP 能力大类注册/查询;名字是**扁平**的(MCP 无 namespace 概念),
|
|
5
|
+
重名在注册时就报错(fail-fast),不做隐式覆盖。
|
|
6
|
+
|
|
7
|
+
注册(外部注入):
|
|
8
|
+
registerMcpTools(() => [{ name, title?, description?, inputSchema?, outputSchema?, annotations?, run }])
|
|
9
|
+
registerMcpResources(() => [{ uri, name, title?, description?, mimeType?, read }])
|
|
10
|
+
registerMcpPrompts(() => [{ name, title?, description?, arguments?, render }])
|
|
11
|
+
registerMcpCompletion(async ({ ref, argument }) => string[])
|
|
12
|
+
|
|
13
|
+
调用(handler 用):
|
|
14
|
+
listTools() / callTool(name, args)
|
|
15
|
+
listResources() / listResourceTemplates() / readResource(uri)
|
|
16
|
+
listPrompts() / getPrompt(name, args)
|
|
17
|
+
complete(params)
|
|
18
|
+
capabilities() // 只声明已注册的大类
|
|
19
|
+
|
|
20
|
+
示例:
|
|
21
|
+
registerMcpTools(() => [{
|
|
22
|
+
name: 'user_me', title: '获取当前用户', inputSchema: { type: 'object', properties: {} },
|
|
23
|
+
run: args => kit.ctx.run('user.me', args),
|
|
24
|
+
}])
|
|
25
|
+
|
|
26
|
+
说明: 需要服务端主动推送的类别(logging、resources/subscribe、subscriptions/listen)依赖 SSE,
|
|
27
|
+
本实现是 POST 单响应传输,故不声明这些能力,调用返回 -32601。
|
|
28
|
+
*/
|
|
29
|
+
import { createCtx } from '@blod-nm/base-ctx'
|
|
30
|
+
|
|
31
|
+
const DEFAULT_INPUT_SCHEMA = { type: 'object', properties: {} }
|
|
32
|
+
const safe = s => String(s).replace(/[^A-Za-z0-9_-]/g, '_') // MCP 名字只允许 [A-Za-z0-9_-]
|
|
33
|
+
export const toolNameOf = name => safe(name)
|
|
34
|
+
|
|
35
|
+
const dup = (kind, name) => {
|
|
36
|
+
const e = new Error(`${kind} 重名: ${name}`)
|
|
37
|
+
e.rpcCode = -32602
|
|
38
|
+
return e
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createMcpRegistry() {
|
|
42
|
+
const ctx = createCtx({}) // tools 的执行总线(自己的 ctx)
|
|
43
|
+
const tools = new Map() // name → def
|
|
44
|
+
const resources = new Map() // uri → def
|
|
45
|
+
const prompts = new Map() // name → def
|
|
46
|
+
let completion = null
|
|
47
|
+
let generation = 0
|
|
48
|
+
|
|
49
|
+
const rebuildTools = () => {
|
|
50
|
+
for (const name of [...ctx.keys()]) if (!tools.has(name)) ctx.unregister(name)
|
|
51
|
+
for (const [name, def] of tools) ctx.register(name, { run: args => def.run(args ?? {}) })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/* ---------- 注册面(整组替换;组内重名 fail-fast) ---------- */
|
|
56
|
+
registerTools(resolve) {
|
|
57
|
+
const next = new Map()
|
|
58
|
+
for (const def of resolve()) {
|
|
59
|
+
const name = toolNameOf(def.name)
|
|
60
|
+
if (next.has(name)) throw dup('tool', name)
|
|
61
|
+
next.set(name, def)
|
|
62
|
+
}
|
|
63
|
+
tools.clear()
|
|
64
|
+
for (const [k, v] of next) tools.set(k, v)
|
|
65
|
+
rebuildTools(); generation++
|
|
66
|
+
},
|
|
67
|
+
registerResources(resolve) {
|
|
68
|
+
const next = new Map()
|
|
69
|
+
for (const def of resolve()) {
|
|
70
|
+
if (next.has(def.uri)) throw dup('resource', def.uri)
|
|
71
|
+
next.set(def.uri, def)
|
|
72
|
+
}
|
|
73
|
+
resources.clear()
|
|
74
|
+
for (const [k, v] of next) resources.set(k, v)
|
|
75
|
+
generation++
|
|
76
|
+
},
|
|
77
|
+
registerPrompts(resolve) {
|
|
78
|
+
const next = new Map()
|
|
79
|
+
for (const def of resolve()) {
|
|
80
|
+
const name = toolNameOf(def.name)
|
|
81
|
+
if (next.has(name)) throw dup('prompt', name)
|
|
82
|
+
next.set(name, def)
|
|
83
|
+
}
|
|
84
|
+
prompts.clear()
|
|
85
|
+
for (const [k, v] of next) prompts.set(k, v)
|
|
86
|
+
generation++
|
|
87
|
+
},
|
|
88
|
+
registerCompletion(fn) { completion = fn; generation++ },
|
|
89
|
+
|
|
90
|
+
/* ---------- 能力声明 ---------- */
|
|
91
|
+
capabilities() {
|
|
92
|
+
const caps = {}
|
|
93
|
+
if (tools.size) caps.tools = {}
|
|
94
|
+
if (resources.size) caps.resources = { subscribe: false } // 无 SSE → 不支持订阅推送
|
|
95
|
+
if (prompts.size) caps.prompts = {}
|
|
96
|
+
if (completion) caps.completions = {}
|
|
97
|
+
return caps
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/* ---------- tools ---------- */
|
|
101
|
+
listTools() {
|
|
102
|
+
return [...tools.entries()].map(([name, def]) => {
|
|
103
|
+
const tool = {
|
|
104
|
+
name,
|
|
105
|
+
title: def.title ?? name,
|
|
106
|
+
description: String(def.description ?? ''),
|
|
107
|
+
inputSchema: def.inputSchema ?? DEFAULT_INPUT_SCHEMA,
|
|
108
|
+
}
|
|
109
|
+
if (def.outputSchema) tool.outputSchema = def.outputSchema
|
|
110
|
+
if (def.annotations) tool.annotations = def.annotations
|
|
111
|
+
return tool
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
callTool(name, args) {
|
|
115
|
+
if (!tools.has(name)) throw Object.assign(new Error(`未知工具: ${name}`), { rpcCode: -32602 })
|
|
116
|
+
return ctx.run(name, args ?? {})
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/* ---------- resources ---------- */
|
|
120
|
+
listResources() {
|
|
121
|
+
return [...resources.values()].map(def => ({
|
|
122
|
+
uri: def.uri,
|
|
123
|
+
name: def.name ?? def.uri,
|
|
124
|
+
...(def.title ? { title: def.title } : {}),
|
|
125
|
+
...(def.description ? { description: def.description } : {}),
|
|
126
|
+
...(def.mimeType ? { mimeType: def.mimeType } : {}),
|
|
127
|
+
}))
|
|
128
|
+
},
|
|
129
|
+
listResourceTemplates() { return [] },
|
|
130
|
+
readResource(uri) {
|
|
131
|
+
const def = resources.get(uri)
|
|
132
|
+
if (!def) throw Object.assign(new Error(`未知资源: ${uri}`), { rpcCode: -32602 })
|
|
133
|
+
return def.read(uri)
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/* ---------- prompts ---------- */
|
|
137
|
+
listPrompts() {
|
|
138
|
+
return [...prompts.entries()].map(([name, def]) => ({
|
|
139
|
+
name,
|
|
140
|
+
...(def.title ? { title: def.title } : {}),
|
|
141
|
+
...(def.description ? { description: def.description } : {}),
|
|
142
|
+
...(def.arguments ? { arguments: def.arguments } : {}),
|
|
143
|
+
}))
|
|
144
|
+
},
|
|
145
|
+
getPrompt(name, args) {
|
|
146
|
+
const def = prompts.get(name)
|
|
147
|
+
if (!def) throw Object.assign(new Error(`未知提示词: ${name}`), { rpcCode: -32602 })
|
|
148
|
+
return def.render(args ?? {})
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/* ---------- completions ---------- */
|
|
152
|
+
async complete(params) {
|
|
153
|
+
if (!completion) throw Object.assign(new Error('未注册补全处理器'), { rpcCode: -32601 })
|
|
154
|
+
return { completion: { values: await completion(params) } }
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/* ---------- 自检 ---------- */
|
|
158
|
+
stats: () => ({ generation, tools: tools.size, resources: resources.size, prompts: prompts.size, completion: !!completion }),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const mcp = createMcpRegistry()
|
|
163
|
+
export const registerMcpTools = resolve => mcp.registerTools(resolve)
|
|
164
|
+
export const registerMcpResources = resolve => mcp.registerResources(resolve)
|
|
165
|
+
export const registerMcpPrompts = resolve => mcp.registerPrompts(resolve)
|
|
166
|
+
export const registerMcpCompletion = fn => mcp.registerCompletion(fn)
|
|
167
|
+
export const listTools = () => mcp.listTools()
|
|
168
|
+
export const callTool = (name, args) => mcp.callTool(name, args)
|
|
169
|
+
export const listResources = () => mcp.listResources()
|
|
170
|
+
export const listResourceTemplates = () => mcp.listResourceTemplates()
|
|
171
|
+
export const readResource = uri => mcp.readResource(uri)
|
|
172
|
+
export const listPrompts = () => mcp.listPrompts()
|
|
173
|
+
export const getPrompt = (name, args) => mcp.getPrompt(name, args)
|
|
174
|
+
export const complete = params => mcp.complete(params)
|
|
175
|
+
export const mcpCapabilities = () => mcp.capabilities()
|
|
176
|
+
export const mcpStats = mcp.stats
|
package/src/module.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
module.js — MCP 协议管道(Nuxt 模块 · 零能力 · 纯 JS)
|
|
3
|
+
|
|
4
|
+
职责: 只挂一个出口与一个注入点;本模块不认识任何业务包,能力全靠外部注入。
|
|
5
|
+
|
|
6
|
+
装载:
|
|
7
|
+
nuxt.config.ts: modules: ['@blod-nm/nuxt-mcp'] // 出口 POST /api/mcp
|
|
8
|
+
|
|
9
|
+
注入(名字扁平,无 namespace;重名 fail-fast):
|
|
10
|
+
// runtime/server/plugins/xxx.js
|
|
11
|
+
registerMcpTools(() => [{ name, title, description, inputSchema, run }])
|
|
12
|
+
registerMcpResources(() => [{ uri, name, mimeType, read }])
|
|
13
|
+
registerMcpPrompts(() => [{ name, description, arguments, render }])
|
|
14
|
+
registerMcpCompletion(async ({ ref, argument }) => string[])
|
|
15
|
+
|
|
16
|
+
数据流:
|
|
17
|
+
POST /api/mcp → server/discover / tools.* / resources.* / prompts.* / completion.* → 注册表执行 → MCP 结果
|
|
18
|
+
*/
|
|
19
|
+
import { defineNuxtModule, addServerHandler, addServerImports, createResolver } from '@nuxt/kit'
|
|
20
|
+
|
|
21
|
+
export default defineNuxtModule({
|
|
22
|
+
meta: { name: 'nuxt-mcp', configKey: 'mcp' },
|
|
23
|
+
defaults: { route: '/api/mcp' },
|
|
24
|
+
setup(options, nuxt) {
|
|
25
|
+
const { resolve } = createResolver(import.meta.url)
|
|
26
|
+
addServerHandler({ route: options.route, method: 'post', handler: resolve('../runtime/server/api/mcp.post.js') })
|
|
27
|
+
// 注入面只暴露注册函数;能力注册表本身不出模块
|
|
28
|
+
const from = resolve('../runtime/server/utils/mcp.js')
|
|
29
|
+
addServerImports([
|
|
30
|
+
{ name: 'registerMcpTools', from },
|
|
31
|
+
{ name: 'registerMcpResources', from },
|
|
32
|
+
{ name: 'registerMcpPrompts', from },
|
|
33
|
+
{ name: 'registerMcpCompletion', from },
|
|
34
|
+
])
|
|
35
|
+
nuxt.options.runtimeConfig.mcp = { route: options.route }
|
|
36
|
+
},
|
|
37
|
+
})
|