dsh-mesh-node 0.0.1 → 0.0.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/src/server.js CHANGED
@@ -1,313 +1,317 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks'
2
- import { createServer as createHttpServer } from 'node:http'
3
- import { createServer as createHttpsServer } from 'node:https'
4
- import { McpServer, createMcpHandler } from '@modelcontextprotocol/server'
5
- import { toNodeHandler } from '@modelcontextprotocol/node'
6
- import { MESH_VERSION, verifyBearer, createConfigLoader, createSecretsLoader, meshHome, addInboundToken, peerAclForToken } from './core/config.js'
7
- import { makeAudit, auditPath } from './core/audit.js'
8
- import { JobRunner } from './core/jobs.js'
9
- import { loadTlsMaterial, describeCert } from './core/tls.js'
10
- import { claimPairingCode, readPairingState } from './core/pairing.js'
11
- import { dispatch, isAllowed, applyPeerAcl, registerTools, toolNames } from './tools/index.js'
12
-
13
- const NODE_STARTED_AT = new Date().toISOString()
14
-
15
- const INSTRUCTIONS = `This endpoint is a dsh-mesh node: another developer machine exposed over the Model Context Protocol.
16
- Call mesh_self first to learn what the peer can do and which tools your token may use.
17
- All paths are paths on the PEER machine, not on yours.
18
- Long operations: use task_submit (with an idem key) then poll task_status, instead of risking a call timeout.
19
- shell_run is opt-in and normally refused.`
20
-
21
- /** Carries the authenticated token id down into the per-request MCP server factory. */
22
- const requestScope = new AsyncLocalStorage()
23
-
24
- export function buildMcpServer({ ctx, log = console }) {
25
- const config = ctx.getConfig()
26
- const server = new McpServer(
27
- { name: `dsh-mesh/${config.node?.name || 'local'}`, version: MESH_VERSION },
28
- { instructions: INSTRUCTIONS }
29
- )
30
- registerTools(server, ctx)
31
- return server
32
- }
33
-
34
- function sendJson(res, status, body, extraHeaders = {}) {
35
- const text = JSON.stringify(body)
36
- res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...extraHeaders })
37
- res.end(text)
38
- }
39
-
40
- /** Read a small JSON body (pairing requests are tiny; anything huge is a mistake). */
41
- function readJsonBody(req, limitBytes = 8192) {
42
- return new Promise((resolvePromise, rejectPromise) => {
43
- let size = 0
44
- const chunks = []
45
- req.on('data', (chunk) => {
46
- size += chunk.length
47
- if (size > limitBytes) {
48
- rejectPromise(new Error('body too large'))
49
- req.destroy()
50
- return
51
- }
52
- chunks.push(chunk)
53
- })
54
- req.on('end', () => {
55
- const text = Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/, '').trim()
56
- if (text === '') return resolvePromise({})
57
- try {
58
- resolvePromise(JSON.parse(text))
59
- } catch {
60
- rejectPromise(new Error('body is not valid JSON'))
61
- }
62
- })
63
- req.on('error', rejectPromise)
64
- })
65
- }
66
-
67
- /** Peer ids double as token ids; keep them filename/log friendly. */
68
- function sanitizePeerId(raw) {
69
- const id = String(raw || '')
70
- .trim()
71
- .replace(/[^A-Za-z0-9._-]/g, '-')
72
- .replace(/-+/g, '-')
73
- .slice(0, 48)
74
- return id === '' ? null : id
75
- }
76
-
77
- /**
78
- * Start the mesh HTTP endpoint.
79
- * @param {object} opts
80
- * @param {object} [opts.config] Fixed config (tests). Omit to load and hot-reload from disk.
81
- * @param {object} opts.secrets Inbound tokens.
82
- * @param {string} [opts.home] Mesh home directory (defaults to ~/.dsh-mesh).
83
- */
84
- export async function startMeshServer({ config, secrets, log = console, port, bind, tls, home } = {}) {
85
- const meshHomeDir = home || meshHome()
86
- const getConfig = config ? () => config : createConfigLoader({ home: meshHomeDir, log })
87
- // Secrets hot-reload too: rotating a token takes effect without a restart.
88
- const getSecrets = secrets ? () => secrets : createSecretsLoader({ home: meshHomeDir, log })
89
- const audit = makeAudit({ home: meshHomeDir, log })
90
-
91
- const initial = getConfig()
92
- const jobs = new JobRunner({
93
- home: meshHomeDir,
94
- audit,
95
- log,
96
- limits: initial.limits || {},
97
- exec: (tool, args, opts) => dispatch(ctx, tool, args, opts)
98
- })
99
-
100
- // Shared context. `peerId` is filled per request from the verified bearer token.
101
- const ctx = {
102
- home: meshHomeDir,
103
- secrets,
104
- getSecrets,
105
- log,
106
- audit,
107
- jobs,
108
- getConfig,
109
- isAllowed,
110
- toolNames: () => toolNames(ctx),
111
- dispatch: (name, args, opts) => dispatch(ctx, name, args, opts)
112
- }
113
-
114
- const nodeCfg = initial.node || {}
115
- const listenPort = port ?? nodeCfg.port ?? 8124
116
- const listenBind = bind ?? nodeCfg.bind ?? '127.0.0.1'
117
- const mcpPath = nodeCfg.path || '/mcp'
118
- const tlsCfg = tls ?? nodeCfg.tls ?? { enabled: false }
119
-
120
- let calls = 0
121
- const mcpHandler = createMcpHandler(
122
- () => {
123
- const scope = requestScope.getStore()
124
- const peerAcl = scope?.peerAcl || null
125
- return buildMcpServer({
126
- ctx: {
127
- ...ctx,
128
- peerId: scope?.tokenId,
129
- peerAcl,
130
- getConfig: () => applyPeerAcl(getConfig(), peerAcl)
131
- },
132
- log
133
- })
134
- },
135
- { onerror: (error) => log?.error?.(`[dsh-mesh] handler error: ${error?.message || error}`) }
136
- )
137
- const nodeHandler = toNodeHandler(mcpHandler)
138
-
139
- const requestListener = async (req, res) => {
140
- let pathname = '/'
141
- try {
142
- pathname = new URL(req.url, 'http://mesh.local').pathname
143
- } catch {
144
- /* malformed URL falls through to 404 */
145
- }
146
-
147
- if (pathname === '/health') {
148
- const liveConfig = getConfig()
149
- const pairingState = readPairingState(meshHomeDir)
150
- sendJson(res, 200, {
151
- ok: true,
152
- service: 'dsh-mesh',
153
- version: MESH_VERSION,
154
- node: liveConfig.node?.name || 'local',
155
- /** Local DSH instances this node can bridge to (cluster addressing: <node>:<instance>). */
156
- hosts: (liveConfig.hosts || []).map((entry) => ({
157
- id: entry.id,
158
- url: entry.url,
159
- enabled: entry.enabled !== false
160
- })),
161
- uptimeSec: Math.round((Date.now() - Date.parse(NODE_STARTED_AT)) / 1000),
162
- pid: process.pid,
163
- startedAt: NODE_STARTED_AT,
164
- tools: toolNames(ctx).length,
165
- jobs: jobs.stats(),
166
- tls: tlsCfg?.enabled === true,
167
- pairing: {
168
- enabled: liveConfig.pairing?.enabled === true,
169
- codeActive: Boolean(pairingState && !pairingState.used && !pairingState.expired),
170
- expiresAt: pairingState?.expiresAt ?? null
171
- },
172
- now: new Date().toISOString()
173
- })
174
- return
175
- }
176
-
177
- // Unauthenticated by design: a peer has no token yet, it redeems a one-time code instead.
178
- if (pathname === '/pair') {
179
- if (req.method !== 'POST') {
180
- sendJson(res, 405, { code: 'E_INVALID_ARG', message: 'use POST /pair' }, { allow: 'POST' })
181
- return
182
- }
183
- const liveConfig = getConfig()
184
- if (liveConfig.pairing?.enabled !== true) {
185
- sendJson(res, 403, { code: 'E_DENIED', message: 'pairing is disabled on this node' })
186
- return
187
- }
188
- let body
189
- try {
190
- body = await readJsonBody(req)
191
- } catch (error) {
192
- sendJson(res, 400, { code: 'E_INVALID_ARG', message: error?.message || 'bad body' })
193
- return
194
- }
195
- const claim = claimPairingCode({
196
- home: meshHomeDir,
197
- code: body?.code,
198
- maxAttempts: Number(liveConfig.pairing?.maxAttempts) || 5
199
- })
200
- if (!claim.ok) {
201
- // Small constant delay so the answer time does not reveal much; never say why.
202
- await new Promise((r) => setTimeout(r, 200))
203
- await audit({ peer: 'pair', tool: 'pair', ok: false, code: claim.reason, args: ['code'] })
204
- sendJson(
205
- res,
206
- 401,
207
- {
208
- code: claim.reason === 'E_MAX_ATTEMPTS' ? 'E_MAX_ATTEMPTS' : 'E_AUTH',
209
- message: 'pairing code rejected',
210
- retryable: claim.reason !== 'E_MAX_ATTEMPTS' && claim.reason !== 'E_CODE_USED'
211
- },
212
- { 'www-authenticate': 'Bearer realm="dsh-mesh"' }
213
- )
214
- return
215
- }
216
- const baseId = sanitizePeerId(body?.peerName) || `peer-${Date.now().toString(36)}`
217
- let created
218
- try {
219
- created = addInboundToken({
220
- home: meshHomeDir,
221
- id: baseId,
222
- note: `paired ${new Date().toISOString()}`,
223
- acl: body?.acl
224
- })
225
- } catch {
226
- created = addInboundToken({
227
- home: meshHomeDir,
228
- id: `${baseId}-${Math.random().toString(36).slice(2, 6)}`,
229
- note: `paired ${new Date().toISOString()}`,
230
- acl: body?.acl
231
- })
232
- }
233
- await audit({ peer: created.id, tool: 'pair', ok: true, args: ['code', 'peerName'] })
234
- const scheme = tlsCfg?.enabled === true ? 'https' : 'http'
235
- const address = httpServer.address()
236
- const actualPort = typeof address === 'object' && address ? address.port : listenPort
237
- sendJson(res, 200, {
238
- ok: true,
239
- token: created.token,
240
- tokenId: created.id,
241
- node: liveConfig.node?.name || 'local',
242
- version: MESH_VERSION,
243
- url: `${scheme}://${listenBind}:${actualPort}${mcpPath}`,
244
- certFingerprint: tlsCfg?.enabled === true ? describeCert(meshHomeDir)?.fingerprint256 ?? null : null,
245
- tools: toolNames(ctx).length
246
- })
247
- return
248
- }
249
-
250
- if (pathname !== mcpPath) {
251
- sendJson(res, 404, { code: 'E_NOT_FOUND', message: `no route for ${pathname}`, mcpEndpoint: mcpPath })
252
- return
253
- }
254
-
255
- const secretsNow = getSecrets()
256
- const tokenId = verifyBearer(req, secretsNow)
257
- if (!tokenId) {
258
- sendJson(
259
- res,
260
- 401,
261
- { code: 'E_AUTH', message: 'missing or invalid bearer token' },
262
- { 'www-authenticate': 'Bearer realm="dsh-mesh"' }
263
- )
264
- return
265
- }
266
- const peerAcl = peerAclForToken(secretsNow, tokenId)
267
-
268
- calls += 1
269
- const started = Date.now()
270
- res.on('close', () => {
271
- log?.debug?.(`[dsh-mesh] ${req.method} ${pathname} token=${tokenId} ${res.statusCode} ${Date.now() - started}ms`)
272
- })
273
-
274
- try {
275
- await requestScope.run({ tokenId, peerAcl }, () => nodeHandler(req, res))
276
- } catch (error) {
277
- log?.error?.(`[dsh-mesh] transport failure: ${error?.message || error}`)
278
- if (!res.headersSent) sendJson(res, 500, { code: 'E_INTERNAL', message: 'transport failure' })
279
- }
280
- }
281
-
282
- const httpServer =
283
- tlsCfg?.enabled === true
284
- ? createHttpsServer(loadTlsMaterial({ home: meshHomeDir, cert: tlsCfg.cert, key: tlsCfg.key }), requestListener)
285
- : createHttpServer(requestListener)
286
-
287
- await new Promise((resolvePromise, rejectPromise) => {
288
- httpServer.once('error', rejectPromise)
289
- httpServer.listen(listenPort, listenBind, () => {
290
- httpServer.off('error', rejectPromise)
291
- resolvePromise()
292
- })
293
- })
294
-
295
- const address = httpServer.address()
296
- const actualPort = typeof address === 'object' && address ? address.port : listenPort
297
- const scheme = tlsCfg?.enabled === true ? 'https' : 'http'
298
- const url = `${scheme}://${listenBind}:${actualPort}${mcpPath}`
299
-
300
- return {
301
- port: actualPort,
302
- url,
303
- httpServer,
304
- ctx,
305
- auditPath: auditPath(meshHomeDir),
306
- get requestCount() {
307
- return calls
308
- },
309
- close() {
310
- return new Promise((resolvePromise) => httpServer.close(() => resolvePromise()))
311
- }
312
- }
313
- }
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { createServer as createHttpServer } from 'node:http'
3
+ import { createServer as createHttpsServer } from 'node:https'
4
+ import { McpServer, createMcpHandler } from '@modelcontextprotocol/server'
5
+ import { toNodeHandler } from '@modelcontextprotocol/node'
6
+ import { MESH_VERSION, verifyBearer, createConfigLoader, createSecretsLoader, meshHome, addInboundToken, peerAclForToken } from './core/config.js'
7
+ import { makeAudit, auditPath } from './core/audit.js'
8
+ import { JobRunner } from './core/jobs.js'
9
+ import { loadTlsMaterial, describeCert } from './core/tls.js'
10
+ import { claimPairingCode, readPairingState } from './core/pairing.js'
11
+ import { dispatch, isAllowed, applyPeerAcl, registerTools, toolNames } from './tools/index.js'
12
+ import { createTunnelCache } from './core/tunnels.js'
13
+
14
+ const NODE_STARTED_AT = new Date().toISOString()
15
+
16
+ const INSTRUCTIONS = `This endpoint is a dsh-mesh node: another developer machine exposed over the Model Context Protocol.
17
+ Call mesh_self first to learn what the peer can do and which tools your token may use.
18
+ All paths are paths on the PEER machine, not on yours.
19
+ Long operations: use task_submit (with an idem key) then poll task_status, instead of risking a call timeout.
20
+ shell_run is opt-in and normally refused.`
21
+
22
+ /** Carries the authenticated token id down into the per-request MCP server factory. */
23
+ const requestScope = new AsyncLocalStorage()
24
+
25
+ export function buildMcpServer({ ctx, log = console }) {
26
+ const config = ctx.getConfig()
27
+ const server = new McpServer(
28
+ { name: `dsh-mesh/${config.node?.name || 'local'}`, version: MESH_VERSION },
29
+ { instructions: INSTRUCTIONS }
30
+ )
31
+ registerTools(server, ctx)
32
+ return server
33
+ }
34
+
35
+ function sendJson(res, status, body, extraHeaders = {}) {
36
+ const text = JSON.stringify(body)
37
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', ...extraHeaders })
38
+ res.end(text)
39
+ }
40
+
41
+ /** Read a small JSON body (pairing requests are tiny; anything huge is a mistake). */
42
+ function readJsonBody(req, limitBytes = 8192) {
43
+ return new Promise((resolvePromise, rejectPromise) => {
44
+ let size = 0
45
+ const chunks = []
46
+ req.on('data', (chunk) => {
47
+ size += chunk.length
48
+ if (size > limitBytes) {
49
+ rejectPromise(new Error('body too large'))
50
+ req.destroy()
51
+ return
52
+ }
53
+ chunks.push(chunk)
54
+ })
55
+ req.on('end', () => {
56
+ const text = Buffer.concat(chunks).toString('utf8').replace(/^\uFEFF/, '').trim()
57
+ if (text === '') return resolvePromise({})
58
+ try {
59
+ resolvePromise(JSON.parse(text))
60
+ } catch {
61
+ rejectPromise(new Error('body is not valid JSON'))
62
+ }
63
+ })
64
+ req.on('error', rejectPromise)
65
+ })
66
+ }
67
+
68
+ /** Peer ids double as token ids; keep them filename/log friendly. */
69
+ function sanitizePeerId(raw) {
70
+ const id = String(raw || '')
71
+ .trim()
72
+ .replace(/[^A-Za-z0-9._-]/g, '-')
73
+ .replace(/-+/g, '-')
74
+ .slice(0, 48)
75
+ return id === '' ? null : id
76
+ }
77
+
78
+ /**
79
+ * Start the mesh HTTP endpoint.
80
+ * @param {object} opts
81
+ * @param {object} [opts.config] Fixed config (tests). Omit to load and hot-reload from disk.
82
+ * @param {object} opts.secrets Inbound tokens.
83
+ * @param {string} [opts.home] Mesh home directory (defaults to ~/.dsh-mesh).
84
+ */
85
+ export async function startMeshServer({ config, secrets, log = console, port, bind, tls, home } = {}) {
86
+ const meshHomeDir = home || meshHome()
87
+ const getConfig = config ? () => config : createConfigLoader({ home: meshHomeDir, log })
88
+ // Secrets hot-reload too: rotating a token takes effect without a restart.
89
+ const getSecrets = secrets ? () => secrets : createSecretsLoader({ home: meshHomeDir, log })
90
+ const audit = makeAudit({ home: meshHomeDir, log })
91
+
92
+ const initial = getConfig()
93
+ // 2026-11-09:隧道状态缓存(自建 frps + Sakura)。/health 携带,60s 后台刷新,不阻塞请求。
94
+ const tunnelCache = createTunnelCache({ repo: initial.repo || process.cwd(), home: meshHomeDir, refreshMs: Math.max(3000, Number(process.env.TUNNEL_CACHE_MS || 10_000)), log }).start()
95
+ const jobs = new JobRunner({
96
+ home: meshHomeDir,
97
+ audit,
98
+ log,
99
+ limits: initial.limits || {},
100
+ exec: (tool, args, opts) => dispatch(ctx, tool, args, opts)
101
+ })
102
+
103
+ // Shared context. `peerId` is filled per request from the verified bearer token.
104
+ const ctx = {
105
+ home: meshHomeDir,
106
+ secrets,
107
+ getSecrets,
108
+ log,
109
+ audit,
110
+ jobs,
111
+ getConfig,
112
+ isAllowed,
113
+ toolNames: () => toolNames(ctx),
114
+ dispatch: (name, args, opts) => dispatch(ctx, name, args, opts)
115
+ }
116
+
117
+ const nodeCfg = initial.node || {}
118
+ const listenPort = port ?? nodeCfg.port ?? 8124
119
+ const listenBind = bind ?? nodeCfg.bind ?? '127.0.0.1'
120
+ const mcpPath = nodeCfg.path || '/mcp'
121
+ const tlsCfg = tls ?? nodeCfg.tls ?? { enabled: false }
122
+
123
+ let calls = 0
124
+ const mcpHandler = createMcpHandler(
125
+ () => {
126
+ const scope = requestScope.getStore()
127
+ const peerAcl = scope?.peerAcl || null
128
+ return buildMcpServer({
129
+ ctx: {
130
+ ...ctx,
131
+ peerId: scope?.tokenId,
132
+ peerAcl,
133
+ getConfig: () => applyPeerAcl(getConfig(), peerAcl)
134
+ },
135
+ log
136
+ })
137
+ },
138
+ { onerror: (error) => log?.error?.(`[dsh-mesh] handler error: ${error?.message || error}`) }
139
+ )
140
+ const nodeHandler = toNodeHandler(mcpHandler)
141
+
142
+ const requestListener = async (req, res) => {
143
+ let pathname = '/'
144
+ try {
145
+ pathname = new URL(req.url, 'http://mesh.local').pathname
146
+ } catch {
147
+ /* malformed URL falls through to 404 */
148
+ }
149
+
150
+ if (pathname === '/health') {
151
+ const liveConfig = getConfig()
152
+ const pairingState = readPairingState(meshHomeDir)
153
+ sendJson(res, 200, {
154
+ ok: true,
155
+ service: 'dsh-mesh',
156
+ version: MESH_VERSION,
157
+ node: liveConfig.node?.name || 'local',
158
+ /** Local DSH instances this node can bridge to (cluster addressing: <node>:<instance>). */
159
+ hosts: (liveConfig.hosts || []).map((entry) => ({
160
+ id: entry.id,
161
+ url: entry.url,
162
+ enabled: entry.enabled !== false
163
+ })),
164
+ uptimeSec: Math.round((Date.now() - Date.parse(NODE_STARTED_AT)) / 1000),
165
+ pid: process.pid,
166
+ startedAt: NODE_STARTED_AT,
167
+ tools: toolNames(ctx).length,
168
+ jobs: jobs.stats(),
169
+ tls: tlsCfg?.enabled === true,
170
+ tunnels: tunnelCache.get(),
171
+ pairing: {
172
+ enabled: liveConfig.pairing?.enabled === true,
173
+ codeActive: Boolean(pairingState && !pairingState.used && !pairingState.expired),
174
+ expiresAt: pairingState?.expiresAt ?? null
175
+ },
176
+ now: new Date().toISOString()
177
+ })
178
+ return
179
+ }
180
+
181
+ // Unauthenticated by design: a peer has no token yet, it redeems a one-time code instead.
182
+ if (pathname === '/pair') {
183
+ if (req.method !== 'POST') {
184
+ sendJson(res, 405, { code: 'E_INVALID_ARG', message: 'use POST /pair' }, { allow: 'POST' })
185
+ return
186
+ }
187
+ const liveConfig = getConfig()
188
+ if (liveConfig.pairing?.enabled !== true) {
189
+ sendJson(res, 403, { code: 'E_DENIED', message: 'pairing is disabled on this node' })
190
+ return
191
+ }
192
+ let body
193
+ try {
194
+ body = await readJsonBody(req)
195
+ } catch (error) {
196
+ sendJson(res, 400, { code: 'E_INVALID_ARG', message: error?.message || 'bad body' })
197
+ return
198
+ }
199
+ const claim = claimPairingCode({
200
+ home: meshHomeDir,
201
+ code: body?.code,
202
+ maxAttempts: Number(liveConfig.pairing?.maxAttempts) || 5
203
+ })
204
+ if (!claim.ok) {
205
+ // Small constant delay so the answer time does not reveal much; never say why.
206
+ await new Promise((r) => setTimeout(r, 200))
207
+ await audit({ peer: 'pair', tool: 'pair', ok: false, code: claim.reason, args: ['code'] })
208
+ sendJson(
209
+ res,
210
+ 401,
211
+ {
212
+ code: claim.reason === 'E_MAX_ATTEMPTS' ? 'E_MAX_ATTEMPTS' : 'E_AUTH',
213
+ message: 'pairing code rejected',
214
+ retryable: claim.reason !== 'E_MAX_ATTEMPTS' && claim.reason !== 'E_CODE_USED'
215
+ },
216
+ { 'www-authenticate': 'Bearer realm="dsh-mesh"' }
217
+ )
218
+ return
219
+ }
220
+ const baseId = sanitizePeerId(body?.peerName) || `peer-${Date.now().toString(36)}`
221
+ let created
222
+ try {
223
+ created = addInboundToken({
224
+ home: meshHomeDir,
225
+ id: baseId,
226
+ note: `paired ${new Date().toISOString()}`,
227
+ acl: body?.acl
228
+ })
229
+ } catch {
230
+ created = addInboundToken({
231
+ home: meshHomeDir,
232
+ id: `${baseId}-${Math.random().toString(36).slice(2, 6)}`,
233
+ note: `paired ${new Date().toISOString()}`,
234
+ acl: body?.acl
235
+ })
236
+ }
237
+ await audit({ peer: created.id, tool: 'pair', ok: true, args: ['code', 'peerName'] })
238
+ const scheme = tlsCfg?.enabled === true ? 'https' : 'http'
239
+ const address = httpServer.address()
240
+ const actualPort = typeof address === 'object' && address ? address.port : listenPort
241
+ sendJson(res, 200, {
242
+ ok: true,
243
+ token: created.token,
244
+ tokenId: created.id,
245
+ node: liveConfig.node?.name || 'local',
246
+ version: MESH_VERSION,
247
+ url: `${scheme}://${listenBind}:${actualPort}${mcpPath}`,
248
+ certFingerprint: tlsCfg?.enabled === true ? describeCert(meshHomeDir)?.fingerprint256 ?? null : null,
249
+ tools: toolNames(ctx).length
250
+ })
251
+ return
252
+ }
253
+
254
+ if (pathname !== mcpPath) {
255
+ sendJson(res, 404, { code: 'E_NOT_FOUND', message: `no route for ${pathname}`, mcpEndpoint: mcpPath })
256
+ return
257
+ }
258
+
259
+ const secretsNow = getSecrets()
260
+ const tokenId = verifyBearer(req, secretsNow)
261
+ if (!tokenId) {
262
+ sendJson(
263
+ res,
264
+ 401,
265
+ { code: 'E_AUTH', message: 'missing or invalid bearer token' },
266
+ { 'www-authenticate': 'Bearer realm="dsh-mesh"' }
267
+ )
268
+ return
269
+ }
270
+ const peerAcl = peerAclForToken(secretsNow, tokenId)
271
+
272
+ calls += 1
273
+ const started = Date.now()
274
+ res.on('close', () => {
275
+ log?.debug?.(`[dsh-mesh] ${req.method} ${pathname} token=${tokenId} ${res.statusCode} ${Date.now() - started}ms`)
276
+ })
277
+
278
+ try {
279
+ await requestScope.run({ tokenId, peerAcl }, () => nodeHandler(req, res))
280
+ } catch (error) {
281
+ log?.error?.(`[dsh-mesh] transport failure: ${error?.message || error}`)
282
+ if (!res.headersSent) sendJson(res, 500, { code: 'E_INTERNAL', message: 'transport failure' })
283
+ }
284
+ }
285
+
286
+ const httpServer =
287
+ tlsCfg?.enabled === true
288
+ ? createHttpsServer(loadTlsMaterial({ home: meshHomeDir, cert: tlsCfg.cert, key: tlsCfg.key }), requestListener)
289
+ : createHttpServer(requestListener)
290
+
291
+ await new Promise((resolvePromise, rejectPromise) => {
292
+ httpServer.once('error', rejectPromise)
293
+ httpServer.listen(listenPort, listenBind, () => {
294
+ httpServer.off('error', rejectPromise)
295
+ resolvePromise()
296
+ })
297
+ })
298
+
299
+ const address = httpServer.address()
300
+ const actualPort = typeof address === 'object' && address ? address.port : listenPort
301
+ const scheme = tlsCfg?.enabled === true ? 'https' : 'http'
302
+ const url = `${scheme}://${listenBind}:${actualPort}${mcpPath}`
303
+
304
+ return {
305
+ port: actualPort,
306
+ url,
307
+ httpServer,
308
+ ctx,
309
+ auditPath: auditPath(meshHomeDir),
310
+ get requestCount() {
311
+ return calls
312
+ },
313
+ close() {
314
+ return new Promise((resolvePromise) => httpServer.close(() => resolvePromise()))
315
+ }
316
+ }
317
+ }