dsh-as-mcp-server 0.1.1 → 0.1.3

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 CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-mcp-bridge
1
+ # dsh-as-mcp-server
2
2
 
3
3
  **Exposes DSH as an MCP server**, so any MCP client can watch what the agent is doing and steer it mid-flight.
4
4
 
package/dsh/index.js CHANGED
@@ -244,7 +244,7 @@ export function apply(ctx, config = {}) {
244
244
  id: alert.id, // 直接用 UUID(绝对唯一),去掉 `||` 短路,避免回退到可碰撞的固定 id
245
245
  role: 'user',
246
246
  content: [{ type: 'text', text: `[${alert.severity}] MCP 客户端 实时提醒:${alert.message}` }],
247
- source: { kind: 'user', plugin: 'dsh-mcp-bridge', clientTimeZone: 'Asia/Shanghai' },
247
+ source: { kind: 'user', plugin: 'dsh-mcp-bridge', clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone },
248
248
  }
249
249
  return { ...decision, messages: [...decision.messages, msg] }
250
250
  }
package/dsh/mcp.js CHANGED
@@ -1,318 +1,324 @@
1
- // dsh/mcp.js —— DSH ↔ MCP 实时会诊桥的 MCP server(Streamable HTTP,多会话:支持记录器 + MCP 客户端 并存)
2
- //
3
- // 对外暴露工具(MCP 客户端调):
4
- // dsh_get_status / dsh_get_intent / dsh_set_intent —— 实时状态 + 共享用户意图(开会)
5
- // dsh_lock_acquire / dsh_lock_release / dsh_lock_status —— 地板锁(单位时间只许一个智能体动工作区)
6
- // dsh_inject_alert —— 中断提醒(插进 DSH 正在进行的这一轮)
7
- // dsh_review_request —— 审查委托(写入收件箱,wb_inbox 可接)
8
- //
9
- // 事件通知(server → client,实时流):
10
- // notifications/dsh/status notifications/dsh/tool notifications/dsh/file
11
- // notifications/dsh/thinking notifications/dsh/intent notifications/dsh/lock
12
- // notifications/dsh/alert-queued notifications/dsh/review
13
- //
14
- // 多会话实现要点(实测修复):
15
- // SDK 的 Server 只能 connect 一个 transport,且单个 StreamableHTTPServerTransport 只服务一个会话,
16
- // 所以「每会话独立 Server+transport」才支持多个客户端并存;notify 遍历所有会话广播。
17
- // 客户端重连必须换新 sessionId(旧 id 会 400 死循环)。
18
- //
19
- // 零第三方依赖:SDK 由插件 node_modules junction 提供;支持 node dsh/mcp.js [port] 独立自测。
20
-
21
- import { randomUUID } from 'node:crypto'
22
- import { createServer } from 'node:http'
23
- import { resolve } from 'node:path'
24
- import { fileURLToPath } from 'node:url'
25
- import { Server } from '@modelcontextprotocol/sdk/server/index.js'
26
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
27
- import {
28
- ListToolsRequestSchema,
29
- CallToolRequestSchema,
30
- McpError,
31
- ErrorCode,
32
- } from '@modelcontextprotocol/sdk/types.js'
33
-
34
- export function buildBridgeTools(shared, hooks, opts = {}) {
35
- const lockTtlMs = Number.isFinite(opts.lockTtlMs) ? opts.lockTtlMs : 300000
36
- const { notify, writeInboxAlert, writeReviewManifest } = hooks
37
- const lockValid = () => shared.lock !== null && shared.lock.expiresAt > Date.now()
38
- const getStatus = () => ({
39
- status: shared.status,
40
- current: shared.current,
41
- turn: shared.turn,
42
- lastTool: shared.lastTool,
43
- problems: (shared.problems || []).slice(-10),
44
- intent: shared.intent,
45
- lock: lockValid() ? shared.lock : null,
46
- alertQueue: (shared.alerts || []).length,
47
- })
48
- return {
49
- dsh_get_status: {
50
- name: 'dsh_get_status',
51
- description: '读取 DSH 当前实时状态:status/current/turn/lastTool/problems/intent/lock/alertQueue',
52
- properties: {},
53
- required: [],
54
- async execute() { return getStatus() },
55
- },
56
- dsh_get_intent: {
57
- name: 'dsh_get_intent',
58
- description: '读取共享用户意图(会议议程):双方实时读到同一份 { text, by, at }',
59
- properties: {},
60
- required: [],
61
- async execute() { return { intent: shared.intent } },
62
- },
63
- dsh_set_intent: {
64
- name: 'dsh_set_intent',
65
- description: '写入/更新共享用户意图(如「用户要 X,请双方围绕 X 配合」)。text 必填,by 标识写入方',
66
- properties: {
67
- text: { type: 'string', description: '意图内容(≤2000 字)' },
68
- by: { type: 'string', description: '写入方标识,默认 MCP 客户端' },
69
- },
70
- required: ['text'],
71
- async execute(args) {
72
- shared.intent = {
73
- text: String(args.text).slice(0, 2000),
74
- by: typeof args.by === 'string' && args.by.trim() ? args.by.slice(0, 50) : 'mcp-client',
75
- at: new Date().toISOString(),
76
- }
77
- notify('notifications/dsh/intent', shared.intent)
78
- return { ok: true, intent: shared.intent }
79
- },
80
- },
81
- dsh_lock_acquire: {
82
- name: 'dsh_lock_acquire',
83
- description: '获取地板锁:持锁期间 DSH 的写入工具(write/edit/git)会被拒绝,实现单位时间单智能体动工作区',
84
- properties: {
85
- owner: { type: 'string', description: '持锁方标识,如 MCP 客户端 或 dsh' },
86
- ttlMs: { type: 'number', description: '锁有效期(毫秒),默认 300000' },
87
- },
88
- required: ['owner'],
89
- async execute(args) {
90
- // H4 修复:owner 消毒——缺省/空串直接报错,避免变成字符串 "undefined" 导致释放比对 mismatch
91
- const ownerRaw = args && typeof args.owner === 'string' ? args.owner.trim() : ''
92
- if (!ownerRaw) throw new Error('dsh_lock_acquire: owner 必填(如 MCP 客户端 或 dsh)')
93
- const owner = ownerRaw.slice(0, 50)
94
- if (lockValid() && shared.lock.owner !== owner) {
95
- return { ok: false, reason: `工作区已被 ${shared.lock.owner} 锁定`, lock: shared.lock }
96
- }
97
- shared.lock = {
98
- owner,
99
- at: new Date().toISOString(),
100
- expiresAt: Date.now() + (Number(args.ttlMs) > 0 ? Number(args.ttlMs) : lockTtlMs),
101
- }
102
- notify('notifications/dsh/lock', shared.lock)
103
- return { ok: true, lock: shared.lock }
104
- },
105
- },
106
- dsh_lock_release: {
107
- name: 'dsh_lock_release',
108
- description: '释放地板锁(仅持锁方自己可释放;锁有过期时间兜底)',
109
- properties: {
110
- owner: { type: 'string', description: '持锁方标识' },
111
- },
112
- required: ['owner'],
113
- async execute(args) {
114
- const ownerRaw = args && typeof args.owner === 'string' ? args.owner.trim() : ''
115
- if (!ownerRaw) throw new Error('dsh_lock_release: owner 必填')
116
- const owner = ownerRaw.slice(0, 50)
117
- if (!lockValid()) return { ok: false, reason: '锁未被持有(可能已过期)' }
118
- if (shared.lock.owner !== owner) {
119
- return { ok: false, reason: `只有持锁方 ${shared.lock.owner} 能释放` }
120
- }
121
- shared.lock = null
122
- notify('notifications/dsh/lock', null)
123
- return { ok: true }
124
- },
125
- },
126
- dsh_lock_status: {
127
- name: 'dsh_lock_status',
128
- description: '查看地板锁当前状态',
129
- properties: {},
130
- required: [],
131
- async execute() { return { held: lockValid(), lock: shared.lock } },
132
- },
133
- dsh_inject_alert: {
134
- name: 'dsh_inject_alert',
135
- description: '向 DSH 当前正在进行的这一轮注入提醒(经 pre-step 瀑布插进消息流)。若注入被禁用则写入收件箱',
136
- properties: {
137
- message: { type: 'string', description: '提醒内容(≤1000 字)' },
138
- severity: { type: 'string', enum: ['info', 'warning', 'error'], description: '级别,默认 info' },
139
- },
140
- required: ['message'],
141
- async execute(args) {
142
- const message = String(args.message).slice(0, 1000)
143
- const severity = args.severity || 'info'
144
- // 幂等:同 message+severity 已在未消费队列则跳过,避免同一提醒反复刷屏
145
- // (呼应 8/22 复盘「不要重复投递同一 alert」)
146
- const existing = shared.alerts.find((a) => a.message === message && a.severity === severity)
147
- if (existing) {
148
- notify('notifications/dsh/alert-queued', existing)
149
- return { ok: true, alert: existing, queued: shared.alerts.length, deduplicated: true }
150
- }
151
- const alert = {
152
- id: randomUUID(), // 全局唯一,跨重启不撞历史 id,根治 alert-1 撞车
153
- message,
154
- severity,
155
- at: new Date().toISOString(),
156
- }
157
- shared.alerts.push(alert)
158
- notify('notifications/dsh/alert-queued', alert)
159
- if (typeof writeInboxAlert === 'function') writeInboxAlert(alert)
160
- return { ok: true, alert, queued: shared.alerts.length }
161
- },
162
- },
163
- dsh_review_request: {
164
- name: 'dsh_review_request',
165
- description: '委托审查:写入收件箱(inbox/<id>.json),DSH 的 wb_inbox 工具会在下一轮领走并按 standards 审查 targets,结果回写 inbox/done/',
166
- properties: {
167
- id: { type: 'string', description: '任务编号(可选,缺省自动生成)' },
168
- title: { type: 'string', description: '审查标题(必填)' },
169
- targets: { type: 'array', items: { type: 'string' }, description: '要审查的文件路径列表' },
170
- standards: { type: 'array', items: { type: 'string' }, description: '审查标准列表' },
171
- testCmd: { type: 'string', description: '建议测试命令(DSH 不替你执行)' },
172
- },
173
- required: ['title'],
174
- async execute(args) {
175
- const manifest = writeReviewManifest(args)
176
- return { ok: true, manifest }
177
- },
178
- },
179
- }
180
- }
181
-
182
- export async function createMcpBridge(handlers, opts = {}, onReady) {
183
- const { port = 51630, host = '127.0.0.1', token = '' } = opts
184
-
185
- const sessions = new Map() // sessionId -> { server, transport }
186
-
187
- const buildServer = () => {
188
- const server = new Server(
189
- { name: 'dsh-mcp-bridge', version: '0.1.1' },
190
- { capabilities: { tools: {} } },
191
- )
192
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
193
- tools: Object.values(handlers.tools).map((t) => ({
194
- name: t.name,
195
- description: t.description || '',
196
- inputSchema: {
197
- type: 'object',
198
- properties: t.properties || {},
199
- required: t.required || [],
200
- },
201
- })),
202
- }))
203
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
204
- const tool = handlers.tools[request.params.name]
205
- if (!tool) throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`)
206
- try {
207
- const result = await tool.execute(request.params.arguments ?? {})
208
- const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
209
- return { content: [{ type: 'text', text }] }
210
- } catch (error) {
211
- return {
212
- content: [{ type: 'text', text: `错误: ${String(error?.message ?? error)}` }],
213
- isError: true,
214
- }
215
- }
216
- })
217
- return server
218
- }
219
-
220
- const httpServer = createServer(async (req, res) => {
221
- if (token) {
222
- const auth = req.headers.authorization || ''
223
- if (auth !== `Bearer ${token}`) {
224
- res.writeHead(401, { 'content-type': 'application/json' })
225
- res.end(JSON.stringify({ error: 'unauthorized' }))
226
- return
227
- }
228
- }
229
-
230
- const sessionId = req.headers['mcp-session-id']
231
- const method = (req.method || '').toUpperCase()
232
-
233
- // 已有会话 → 复用其 transport(POST/GET/DELETE 统一走这里)
234
- if (sessionId && sessions.has(sessionId)) {
235
- sessions.get(sessionId).transport.handleRequest(req, res).catch((error) => {
236
- console.error(`[dsh-mcp-bridge] 请求处理失败: ${String(error?.message ?? error)}`)
237
- if (!res.headersSent) {
238
- res.writeHead(500, { 'content-type': 'text/plain' })
239
- res.end('internal error')
240
- }
241
- })
242
- return
243
- }
244
-
245
- // 新会话:仅允许通过 POST 的 initialize 建立 → 新建独立 Server + transport
246
- if (!sessionId && method === 'POST') {
247
- const server = buildServer()
248
- const transport = new StreamableHTTPServerTransport({
249
- sessionIdGenerator: () => randomUUID(),
250
- onsessioninitialized: (sid) => sessions.set(sid, { server, transport }),
251
- })
252
- transport.onclose = () => { sessions.delete(transport.sessionId) }
253
- await server.connect(transport)
254
- transport.handleRequest(req, res).catch((error) => {
255
- console.error(`[dsh-mcp-bridge] 新会话处理失败: ${String(error?.message ?? error)}`)
256
- if (!res.headersSent) {
257
- res.writeHead(500, { 'content-type': 'text/plain' })
258
- res.end('internal error')
259
- }
260
- })
261
- return
262
- }
263
-
264
- // 无会话且无 initialize → 拒绝
265
- res.writeHead(400, { 'content-type': 'application/json' })
266
- res.end(JSON.stringify({
267
- jsonrpc: '2.0',
268
- error: { code: -32000, message: 'Bad Request: unknown or missing Mcp-Session-Id' },
269
- id: null,
270
- }))
271
- })
272
-
273
- await new Promise((resolveListen, reject) => {
274
- httpServer.once('error', reject)
275
- httpServer.listen(port, host, resolveListen)
276
- })
277
-
278
- const bridge = {
279
- url: `http://${host}:${port}/mcp`,
280
- notify(method, params) {
281
- for (const { server } of sessions.values()) {
282
- try { server.notification({ method, params }) } catch { /* 该会话已无流时忽略 */ }
283
- }
284
- },
285
- async close() {
286
- for (const { transport } of sessions.values()) {
287
- try { await transport.close() } catch { /* 已关闭 */ }
288
- }
289
- await new Promise((r) => httpServer.close(r))
290
- },
291
- }
292
- if (typeof onReady === 'function') onReady(bridge)
293
- return bridge
294
- }
295
-
296
- // ---- 独立自测模式:node dsh/mcp.js [port] ----
297
- const isCli = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
298
- if (isCli) {
299
- const port = Number(process.argv[2] || 51999)
300
- const shared = {
301
- status: 'working', current: '演示模式', turn: 0, lastTool: null,
302
- problems: [], intent: null, lock: null, alerts: [], seq: 0,
303
- }
304
- let emit = () => {}
305
- const tools = buildBridgeTools(shared, {
306
- notify: (m, p) => emit(m, p),
307
- writeInboxAlert: (alert) => { console.log(`[demo] 提醒入队: ${alert.message}`) },
308
- writeReviewManifest: (args) => ({ ...args, id: args.id || 'demo-review', note: 'demo 模式不落盘' }),
309
- })
310
- createMcpBridge({ tools }, { port, host: '127.0.0.1' }, (b) => {
311
- emit = b.notify
312
- console.log(`[demo] MCP bridge 就绪: ${b.url}`)
313
- console.log(`[demo] 用 node dsh/mcp-test.js ${b.url} 自测`)
314
- }).catch((error) => {
315
- console.error('启动失败:', error)
316
- process.exit(1)
317
- })
318
- }
1
+ // dsh/mcp.js —— DSH ↔ MCP 实时会诊桥的 MCP server(Streamable HTTP,多会话:支持记录器 + MCP 客户端 并存)
2
+ //
3
+ // 对外暴露工具(MCP 客户端调):
4
+ // dsh_get_status / dsh_get_intent / dsh_set_intent —— 实时状态 + 共享用户意图(开会)
5
+ // dsh_lock_acquire / dsh_lock_release / dsh_lock_status —— 地板锁(单位时间只许一个智能体动工作区)
6
+ // dsh_inject_alert —— 中断提醒(插进 DSH 正在进行的这一轮)
7
+ // dsh_review_request —— 审查委托(写入收件箱,wb_inbox 可接)
8
+ //
9
+ // 事件通知(server → client,实时流):
10
+ // notifications/dsh/status notifications/dsh/tool notifications/dsh/file
11
+ // notifications/dsh/thinking notifications/dsh/intent notifications/dsh/lock
12
+ // notifications/dsh/alert-queued notifications/dsh/review
13
+ //
14
+ // 多会话实现要点(实测修复):
15
+ // SDK 的 Server 只能 connect 一个 transport,且单个 StreamableHTTPServerTransport 只服务一个会话,
16
+ // 所以「每会话独立 Server+transport」才支持多个客户端并存;notify 遍历所有会话广播。
17
+ // 客户端重连必须换新 sessionId(旧 id 会 400 死循环)。
18
+ //
19
+ // 零第三方依赖:SDK 由插件 node_modules junction 提供;支持 node dsh/mcp.js [port] 独立自测。
20
+
21
+ import { randomUUID } from 'node:crypto'
22
+ import { createServer } from 'node:http'
23
+ import { resolve } from 'node:path'
24
+ import { fileURLToPath } from 'node:url'
25
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
26
+ import { readFileSync } from 'node:fs'
27
+ import { dirname, join } from 'node:path'
28
+
29
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
30
+ import {
31
+ ListToolsRequestSchema,
32
+ CallToolRequestSchema,
33
+ McpError,
34
+ ErrorCode,
35
+ } from '@modelcontextprotocol/sdk/types.js'
36
+
37
+ const __dirname = dirname(fileURLToPath(import.meta.url))
38
+ const PKG_VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version
39
+
40
+ export function buildBridgeTools(shared, hooks, opts = {}) {
41
+ const lockTtlMs = Number.isFinite(opts.lockTtlMs) ? opts.lockTtlMs : 300000
42
+ const { notify, writeInboxAlert, writeReviewManifest } = hooks
43
+ const lockValid = () => shared.lock !== null && shared.lock.expiresAt > Date.now()
44
+ const getStatus = () => ({
45
+ status: shared.status,
46
+ current: shared.current,
47
+ turn: shared.turn,
48
+ lastTool: shared.lastTool,
49
+ problems: (shared.problems || []).slice(-10),
50
+ intent: shared.intent,
51
+ lock: lockValid() ? shared.lock : null,
52
+ alertQueue: (shared.alerts || []).length,
53
+ })
54
+ return {
55
+ dsh_get_status: {
56
+ name: 'dsh_get_status',
57
+ description: '读取 DSH 当前实时状态:status/current/turn/lastTool/problems/intent/lock/alertQueue',
58
+ properties: {},
59
+ required: [],
60
+ async execute() { return getStatus() },
61
+ },
62
+ dsh_get_intent: {
63
+ name: 'dsh_get_intent',
64
+ description: '读取共享用户意图(会议议程):双方实时读到同一份 { text, by, at }',
65
+ properties: {},
66
+ required: [],
67
+ async execute() { return { intent: shared.intent } },
68
+ },
69
+ dsh_set_intent: {
70
+ name: 'dsh_set_intent',
71
+ description: '写入/更新共享用户意图(如「用户要 X,请双方围绕 X 配合」)。text 必填,by 标识写入方',
72
+ properties: {
73
+ text: { type: 'string', description: '意图内容(≤2000 字)' },
74
+ by: { type: 'string', description: '写入方标识,默认 MCP 客户端' },
75
+ },
76
+ required: ['text'],
77
+ async execute(args) {
78
+ shared.intent = {
79
+ text: String(args.text).slice(0, 2000),
80
+ by: typeof args.by === 'string' && args.by.trim() ? args.by.slice(0, 50) : 'mcp-client',
81
+ at: new Date().toISOString(),
82
+ }
83
+ notify('notifications/dsh/intent', shared.intent)
84
+ return { ok: true, intent: shared.intent }
85
+ },
86
+ },
87
+ dsh_lock_acquire: {
88
+ name: 'dsh_lock_acquire',
89
+ description: '获取地板锁:持锁期间 DSH 的写入工具(write/edit/git)会被拒绝,实现单位时间单智能体动工作区',
90
+ properties: {
91
+ owner: { type: 'string', description: '持锁方标识,如 MCP 客户端 或 dsh' },
92
+ ttlMs: { type: 'number', description: '锁有效期(毫秒),默认 300000' },
93
+ },
94
+ required: ['owner'],
95
+ async execute(args) {
96
+ // H4 修复:owner 消毒——缺省/空串直接报错,避免变成字符串 "undefined" 导致释放比对 mismatch
97
+ const ownerRaw = args && typeof args.owner === 'string' ? args.owner.trim() : ''
98
+ if (!ownerRaw) throw new Error('dsh_lock_acquire: owner 必填(如 MCP 客户端 或 dsh)')
99
+ const owner = ownerRaw.slice(0, 50)
100
+ if (lockValid() && shared.lock.owner !== owner) {
101
+ return { ok: false, reason: `工作区已被 ${shared.lock.owner} 锁定`, lock: shared.lock }
102
+ }
103
+ shared.lock = {
104
+ owner,
105
+ at: new Date().toISOString(),
106
+ expiresAt: Date.now() + (Number(args.ttlMs) > 0 ? Number(args.ttlMs) : lockTtlMs),
107
+ }
108
+ notify('notifications/dsh/lock', shared.lock)
109
+ return { ok: true, lock: shared.lock }
110
+ },
111
+ },
112
+ dsh_lock_release: {
113
+ name: 'dsh_lock_release',
114
+ description: '释放地板锁(仅持锁方自己可释放;锁有过期时间兜底)',
115
+ properties: {
116
+ owner: { type: 'string', description: '持锁方标识' },
117
+ },
118
+ required: ['owner'],
119
+ async execute(args) {
120
+ const ownerRaw = args && typeof args.owner === 'string' ? args.owner.trim() : ''
121
+ if (!ownerRaw) throw new Error('dsh_lock_release: owner 必填')
122
+ const owner = ownerRaw.slice(0, 50)
123
+ if (!lockValid()) return { ok: false, reason: '锁未被持有(可能已过期)' }
124
+ if (shared.lock.owner !== owner) {
125
+ return { ok: false, reason: `只有持锁方 ${shared.lock.owner} 能释放` }
126
+ }
127
+ shared.lock = null
128
+ notify('notifications/dsh/lock', null)
129
+ return { ok: true }
130
+ },
131
+ },
132
+ dsh_lock_status: {
133
+ name: 'dsh_lock_status',
134
+ description: '查看地板锁当前状态',
135
+ properties: {},
136
+ required: [],
137
+ async execute() { return { held: lockValid(), lock: shared.lock } },
138
+ },
139
+ dsh_inject_alert: {
140
+ name: 'dsh_inject_alert',
141
+ description: '向 DSH 当前正在进行的这一轮注入提醒(经 pre-step 瀑布插进消息流)。若注入被禁用则写入收件箱',
142
+ properties: {
143
+ message: { type: 'string', description: '提醒内容(≤1000 字)' },
144
+ severity: { type: 'string', enum: ['info', 'warning', 'error'], description: '级别,默认 info' },
145
+ },
146
+ required: ['message'],
147
+ async execute(args) {
148
+ const message = String(args.message).slice(0, 1000)
149
+ const severity = args.severity || 'info'
150
+ // 幂等:同 message+severity 已在未消费队列则跳过,避免同一提醒反复刷屏
151
+ // (呼应 8/22 复盘「不要重复投递同一 alert」)
152
+ const existing = shared.alerts.find((a) => a.message === message && a.severity === severity)
153
+ if (existing) {
154
+ notify('notifications/dsh/alert-queued', existing)
155
+ return { ok: true, alert: existing, queued: shared.alerts.length, deduplicated: true }
156
+ }
157
+ const alert = {
158
+ id: randomUUID(), // 全局唯一,跨重启不撞历史 id,根治 alert-1 撞车
159
+ message,
160
+ severity,
161
+ at: new Date().toISOString(),
162
+ }
163
+ shared.alerts.push(alert)
164
+ notify('notifications/dsh/alert-queued', alert)
165
+ if (typeof writeInboxAlert === 'function') writeInboxAlert(alert)
166
+ return { ok: true, alert, queued: shared.alerts.length }
167
+ },
168
+ },
169
+ dsh_review_request: {
170
+ name: 'dsh_review_request',
171
+ description: '委托审查:写入收件箱(inbox/<id>.json),DSH 的 wb_inbox 工具会在下一轮领走并按 standards 审查 targets,结果回写 inbox/done/',
172
+ properties: {
173
+ id: { type: 'string', description: '任务编号(可选,缺省自动生成)' },
174
+ title: { type: 'string', description: '审查标题(必填)' },
175
+ targets: { type: 'array', items: { type: 'string' }, description: '要审查的文件路径列表' },
176
+ standards: { type: 'array', items: { type: 'string' }, description: '审查标准列表' },
177
+ testCmd: { type: 'string', description: '建议测试命令(DSH 不替你执行)' },
178
+ },
179
+ required: ['title'],
180
+ async execute(args) {
181
+ const manifest = writeReviewManifest(args)
182
+ return { ok: true, manifest }
183
+ },
184
+ },
185
+ }
186
+ }
187
+
188
+ export async function createMcpBridge(handlers, opts = {}, onReady) {
189
+ const { port = 51630, host = '127.0.0.1', token = '' } = opts
190
+
191
+ const sessions = new Map() // sessionId -> { server, transport }
192
+
193
+ const buildServer = () => {
194
+ const server = new Server(
195
+ { name: 'dsh-as-mcp-server', version: PKG_VERSION },
196
+ { capabilities: { tools: {} } },
197
+ )
198
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
199
+ tools: Object.values(handlers.tools).map((t) => ({
200
+ name: t.name,
201
+ description: t.description || '',
202
+ inputSchema: {
203
+ type: 'object',
204
+ properties: t.properties || {},
205
+ required: t.required || [],
206
+ },
207
+ })),
208
+ }))
209
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
210
+ const tool = handlers.tools[request.params.name]
211
+ if (!tool) throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`)
212
+ try {
213
+ const result = await tool.execute(request.params.arguments ?? {})
214
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
215
+ return { content: [{ type: 'text', text }] }
216
+ } catch (error) {
217
+ return {
218
+ content: [{ type: 'text', text: `错误: ${String(error?.message ?? error)}` }],
219
+ isError: true,
220
+ }
221
+ }
222
+ })
223
+ return server
224
+ }
225
+
226
+ const httpServer = createServer(async (req, res) => {
227
+ if (token) {
228
+ const auth = req.headers.authorization || ''
229
+ if (auth !== `Bearer ${token}`) {
230
+ res.writeHead(401, { 'content-type': 'application/json' })
231
+ res.end(JSON.stringify({ error: 'unauthorized' }))
232
+ return
233
+ }
234
+ }
235
+
236
+ const sessionId = req.headers['mcp-session-id']
237
+ const method = (req.method || '').toUpperCase()
238
+
239
+ // 已有会话 → 复用其 transport(POST/GET/DELETE 统一走这里)
240
+ if (sessionId && sessions.has(sessionId)) {
241
+ sessions.get(sessionId).transport.handleRequest(req, res).catch((error) => {
242
+ console.error(`[dsh-mcp-bridge] 请求处理失败: ${String(error?.message ?? error)}`)
243
+ if (!res.headersSent) {
244
+ res.writeHead(500, { 'content-type': 'text/plain' })
245
+ res.end('internal error')
246
+ }
247
+ })
248
+ return
249
+ }
250
+
251
+ // 新会话:仅允许通过 POST 的 initialize 建立 → 新建独立 Server + transport
252
+ if (!sessionId && method === 'POST') {
253
+ const server = buildServer()
254
+ const transport = new StreamableHTTPServerTransport({
255
+ sessionIdGenerator: () => randomUUID(),
256
+ onsessioninitialized: (sid) => sessions.set(sid, { server, transport }),
257
+ })
258
+ transport.onclose = () => { sessions.delete(transport.sessionId) }
259
+ await server.connect(transport)
260
+ transport.handleRequest(req, res).catch((error) => {
261
+ console.error(`[dsh-mcp-bridge] 新会话处理失败: ${String(error?.message ?? error)}`)
262
+ if (!res.headersSent) {
263
+ res.writeHead(500, { 'content-type': 'text/plain' })
264
+ res.end('internal error')
265
+ }
266
+ })
267
+ return
268
+ }
269
+
270
+ // 无会话且无 initialize → 拒绝
271
+ res.writeHead(400, { 'content-type': 'application/json' })
272
+ res.end(JSON.stringify({
273
+ jsonrpc: '2.0',
274
+ error: { code: -32000, message: 'Bad Request: unknown or missing Mcp-Session-Id' },
275
+ id: null,
276
+ }))
277
+ })
278
+
279
+ await new Promise((resolveListen, reject) => {
280
+ httpServer.once('error', reject)
281
+ httpServer.listen(port, host, resolveListen)
282
+ })
283
+
284
+ const bridge = {
285
+ url: `http://${host}:${port}/mcp`,
286
+ notify(method, params) {
287
+ for (const { server } of sessions.values()) {
288
+ try { server.notification({ method, params }) } catch { /* 该会话已无流时忽略 */ }
289
+ }
290
+ },
291
+ async close() {
292
+ for (const { transport } of sessions.values()) {
293
+ try { await transport.close() } catch { /* 已关闭 */ }
294
+ }
295
+ await new Promise((r) => httpServer.close(r))
296
+ },
297
+ }
298
+ if (typeof onReady === 'function') onReady(bridge)
299
+ return bridge
300
+ }
301
+
302
+ // ---- 独立自测模式:node dsh/mcp.js [port] ----
303
+ const isCli = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
304
+ if (isCli) {
305
+ const port = Number(process.argv[2] || 51999)
306
+ const shared = {
307
+ status: 'working', current: '演示模式', turn: 0, lastTool: null,
308
+ problems: [], intent: null, lock: null, alerts: [], seq: 0,
309
+ }
310
+ let emit = () => {}
311
+ const tools = buildBridgeTools(shared, {
312
+ notify: (m, p) => emit(m, p),
313
+ writeInboxAlert: (alert) => { console.log(`[demo] 提醒入队: ${alert.message}`) },
314
+ writeReviewManifest: (args) => ({ ...args, id: args.id || 'demo-review', note: 'demo 模式不落盘' }),
315
+ })
316
+ createMcpBridge({ tools }, { port, host: '127.0.0.1' }, (b) => {
317
+ emit = b.notify
318
+ console.log(`[demo] MCP bridge 就绪: ${b.url}`)
319
+ console.log(`[demo] 用 node dsh/mcp-test.js ${b.url} 自测`)
320
+ }).catch((error) => {
321
+ console.error('启动失败:', error)
322
+ process.exit(1)
323
+ })
324
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-as-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Exposes DSH as an MCP server over Streamable HTTP: streaming thinking/tool/file notifications, shared intent, a floor lock that physically denies DSH writes while held, mid-turn alert injection, and review delegation.",
5
5
  "type": "module",
6
6
  "main": "./dsh/index.js",
@@ -16,9 +16,9 @@
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/qian-dsh/dsh-mcp-bridge.git"
19
+ "url": "git+https://github.com/qian-dsh/dsh-as-mcp-server.git"
20
20
  },
21
- "homepage": "https://github.com/qian-dsh/dsh-mcp-bridge#readme",
21
+ "homepage": "https://github.com/qian-dsh/dsh-as-mcp-server#readme",
22
22
  "engines": {
23
23
  "node": ">=22.19"
24
24
  },