@kb-labs/mcp-app 2.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +117406 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +592 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/manifest.json +26 -0
- package/package.json +51 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { platform, getProjectRoot, getPlatformRoot, createServiceBootstrap } from '@kb-labs/core-runtime';
|
|
2
|
+
import { makeAssemblyHook } from '@kb-labs/plugin-runtime';
|
|
3
|
+
import { resolvePolicy, can } from '@kb-labs/core-policy';
|
|
4
|
+
import { createCorrelatedLogger, resolveObservabilityInstanceId, createDaemonServer, getListenOptions, OperationMetricsTracker, createServiceObservabilityDescribe, createServiceObservabilityHealth } from '@kb-labs/shared-http';
|
|
5
|
+
import { noopUI, getHandlerPermissions } from '@kb-labs/plugin-contracts';
|
|
6
|
+
import { runDaemon } from '@kb-labs/shared-daemon';
|
|
7
|
+
import { AuthService } from '@kb-labs/gateway-auth';
|
|
8
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
import { createRegistry } from '@kb-labs/core-registry';
|
|
13
|
+
import { generateCommandSchema } from '@kb-labs/cli-commands';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { executeCommandV3 } from '@kb-labs/cli-runtime';
|
|
17
|
+
import { performance } from 'perf_hooks';
|
|
18
|
+
|
|
19
|
+
// src/bootstrap.ts
|
|
20
|
+
var DEV_JWT_SECRET = "dev-insecure-secret-change-me";
|
|
21
|
+
function loadJwtConfig() {
|
|
22
|
+
const secret = process.env.GATEWAY_JWT_SECRET;
|
|
23
|
+
if (!secret && process.env.NODE_ENV === "production") {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`GATEWAY_JWT_SECRET must be set in production. Generate one with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return { secret: secret ?? DEV_JWT_SECRET };
|
|
29
|
+
}
|
|
30
|
+
function extractBearer(header) {
|
|
31
|
+
if (header === void 0 || !/^bearer\s/i.test(header)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const token = header.slice("bearer".length).trim();
|
|
35
|
+
return token.length > 0 ? token : null;
|
|
36
|
+
}
|
|
37
|
+
async function resolveAuthContext(header, cache, jwtConfig) {
|
|
38
|
+
const token = extractBearer(header);
|
|
39
|
+
if (!token) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return new AuthService(cache, jwtConfig).verify(token);
|
|
43
|
+
}
|
|
44
|
+
var callOutput = new AsyncLocalStorage();
|
|
45
|
+
function stringifyError(error) {
|
|
46
|
+
return typeof error === "string" ? error : error.message;
|
|
47
|
+
}
|
|
48
|
+
function createBufferedUI(pushFn) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const push = (text) => {
|
|
51
|
+
if (pushFn) {
|
|
52
|
+
pushFn(text);
|
|
53
|
+
} else {
|
|
54
|
+
lines.push(text);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const ui = {
|
|
58
|
+
colors: noopUI.colors,
|
|
59
|
+
symbols: noopUI.symbols,
|
|
60
|
+
write: (text) => push(text),
|
|
61
|
+
info: (message) => push(`[info] ${message}`),
|
|
62
|
+
success: (message) => push(`[ok] ${message}`),
|
|
63
|
+
warn: (message) => push(`[warn] ${message}`),
|
|
64
|
+
error: (error) => push(`[error] ${stringifyError(error)}`),
|
|
65
|
+
debug: (message) => push(`[debug] ${message}`),
|
|
66
|
+
spinner: (message) => {
|
|
67
|
+
push(`[spinner] ${message}`);
|
|
68
|
+
return {
|
|
69
|
+
update: (m) => push(`[spinner] ${m}`),
|
|
70
|
+
succeed: (m) => push(`[ok] ${m ?? ""}`.trimEnd()),
|
|
71
|
+
fail: (m) => push(`[fail] ${m ?? ""}`.trimEnd()),
|
|
72
|
+
stop: () => {
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
table: (data) => push(JSON.stringify(data)),
|
|
77
|
+
json: (data) => push(JSON.stringify(data)),
|
|
78
|
+
newline: () => push(""),
|
|
79
|
+
divider: () => push("---"),
|
|
80
|
+
box: (content, title) => push(title ? `[${title}]
|
|
81
|
+
${content}` : content),
|
|
82
|
+
sideBox: (options) => {
|
|
83
|
+
push(`[${options.title}]`);
|
|
84
|
+
if (options.summary) {
|
|
85
|
+
push(JSON.stringify(options.summary));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
chain: (items) => items.forEach((item) => push(`\u2022 ${item.title}`)),
|
|
89
|
+
log: (entry) => push(`[${entry.level}] ${entry.message}`),
|
|
90
|
+
// Non-interactive defaults — same semantics as noopUI.
|
|
91
|
+
confirm: async (message, options) => {
|
|
92
|
+
push(`[confirm] ${message}`);
|
|
93
|
+
return options?.defaultValue ?? true;
|
|
94
|
+
},
|
|
95
|
+
prompt: async (message, options) => {
|
|
96
|
+
push(`[prompt] ${message}`);
|
|
97
|
+
return options?.default ?? "";
|
|
98
|
+
},
|
|
99
|
+
select: async (message, choices) => {
|
|
100
|
+
push(`[select] ${message}`);
|
|
101
|
+
return choices[0]?.value;
|
|
102
|
+
},
|
|
103
|
+
multiSelect: async (message, choices) => {
|
|
104
|
+
push(`[multiSelect] ${message}`);
|
|
105
|
+
return choices.filter((c) => c.checked).map((c) => c.value);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
return { ui, getOutput: () => lines.join("\n") };
|
|
109
|
+
}
|
|
110
|
+
function toIdentity(auth) {
|
|
111
|
+
return { user: auth.userId, roles: [auth.type, auth.tier] };
|
|
112
|
+
}
|
|
113
|
+
function actionForOperation(operationType) {
|
|
114
|
+
return operationType === "read" ? "mcp.read" : "mcp.write";
|
|
115
|
+
}
|
|
116
|
+
async function createToolRegistry(opts) {
|
|
117
|
+
return createRegistry({
|
|
118
|
+
root: opts.root,
|
|
119
|
+
platformRoot: opts.platformRoot,
|
|
120
|
+
cache: { ttlMs: 6e4, adapter: opts.cache }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function sanitizePluginId(pluginId) {
|
|
124
|
+
return pluginId.replace(/^@/, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
125
|
+
}
|
|
126
|
+
function toolName(pluginId, commandPath) {
|
|
127
|
+
return `${sanitizePluginId(pluginId)}__${commandPath.trim().replace(/\s+/g, "_")}`;
|
|
128
|
+
}
|
|
129
|
+
function toCommandManifest(decl, entry) {
|
|
130
|
+
const segments = decl.path.trim().split(/\s+/).filter(Boolean);
|
|
131
|
+
return {
|
|
132
|
+
manifestVersion: "1.0",
|
|
133
|
+
segments,
|
|
134
|
+
id: segments[segments.length - 1] ?? "",
|
|
135
|
+
group: segments[0] ?? "",
|
|
136
|
+
subgroup: segments.length >= 3 ? segments[1] : void 0,
|
|
137
|
+
describe: decl.describe ?? "",
|
|
138
|
+
longDescription: decl.longDescription,
|
|
139
|
+
aliases: decl.aliases,
|
|
140
|
+
category: decl.category,
|
|
141
|
+
flags: decl.flags,
|
|
142
|
+
examples: decl.examples,
|
|
143
|
+
operationType: decl.operationType,
|
|
144
|
+
package: entry.pluginId,
|
|
145
|
+
manifestV2: entry.manifest,
|
|
146
|
+
pkgRoot: entry.pluginRoot
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function filterTools(snapshot, permits) {
|
|
150
|
+
const tools = [];
|
|
151
|
+
for (const entry of snapshot.manifests) {
|
|
152
|
+
for (const decl of entry.manifest.cli?.commands ?? []) {
|
|
153
|
+
if (!permits(decl.operationType, entry.pluginId)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
tools.push({
|
|
157
|
+
name: toolName(entry.pluginId, decl.path),
|
|
158
|
+
description: decl.describe,
|
|
159
|
+
inputSchema: generateCommandSchema(toCommandManifest(decl, entry)),
|
|
160
|
+
pluginId: entry.pluginId,
|
|
161
|
+
pluginRoot: entry.pluginRoot,
|
|
162
|
+
handlerPath: decl.handler,
|
|
163
|
+
version: entry.manifest.version ?? "0.0.0",
|
|
164
|
+
operationType: decl.operationType,
|
|
165
|
+
permissions: getHandlerPermissions(entry.manifest, "cli", decl.path)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return tools;
|
|
170
|
+
}
|
|
171
|
+
function resolveHandlerPath(pluginRoot, handler) {
|
|
172
|
+
const relative = handler.split("#")[0] ?? handler;
|
|
173
|
+
return relative.startsWith("dist/") ? path.resolve(pluginRoot, relative) : path.resolve(pluginRoot, "dist", relative);
|
|
174
|
+
}
|
|
175
|
+
function createPlatformServices(container) {
|
|
176
|
+
return {
|
|
177
|
+
logger: container.logger,
|
|
178
|
+
llm: container.llm,
|
|
179
|
+
embeddings: container.embeddings,
|
|
180
|
+
vectorStore: container.vectorStore,
|
|
181
|
+
cache: container.cache,
|
|
182
|
+
config: container.config,
|
|
183
|
+
storage: container.storage,
|
|
184
|
+
analytics: container.analytics,
|
|
185
|
+
eventBus: container.eventBus,
|
|
186
|
+
invoke: container.invoke,
|
|
187
|
+
documentDatabase: container.documentDatabase,
|
|
188
|
+
kvStore: container.kvStore,
|
|
189
|
+
logs: container.logs
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function callTool(tool, args, tenantId, resolvePlatform) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
const container = resolvePlatform(tenantId);
|
|
195
|
+
const exitCode = await callOutput.run(
|
|
196
|
+
lines,
|
|
197
|
+
() => executeCommandV3({
|
|
198
|
+
pluginId: tool.pluginId,
|
|
199
|
+
pluginVersion: tool.version,
|
|
200
|
+
pluginRoot: tool.pluginRoot,
|
|
201
|
+
handlerPath: resolveHandlerPath(tool.pluginRoot, tool.handlerPath),
|
|
202
|
+
argv: [],
|
|
203
|
+
flags: args,
|
|
204
|
+
tenantId,
|
|
205
|
+
// ui is ignored by executeCommandV3 V3 (backend uses uiProvider).
|
|
206
|
+
// noopUI is passed explicitly to document this intent.
|
|
207
|
+
ui: noopUI,
|
|
208
|
+
platform: createPlatformServices(container),
|
|
209
|
+
platformContainer: container,
|
|
210
|
+
socketPath: container.getSocketPath(),
|
|
211
|
+
permissions: tool.permissions,
|
|
212
|
+
quotas: tool.permissions?.quotas
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
return { success: exitCode === 0, output: lines.join("\n"), exitCode };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/mcp/request-handler.ts
|
|
219
|
+
var TOOLS_CACHE_TTL_MS = 6e4;
|
|
220
|
+
function textResult(text, isError) {
|
|
221
|
+
return { content: [{ type: "text", text }], isError };
|
|
222
|
+
}
|
|
223
|
+
function toolsCacheKey(authHeader) {
|
|
224
|
+
if (!authHeader) {
|
|
225
|
+
return "mcp:tools:anonymous";
|
|
226
|
+
}
|
|
227
|
+
return `mcp:tools:${createHash("sha256").update(authHeader).digest("hex").slice(0, 16)}`;
|
|
228
|
+
}
|
|
229
|
+
async function resolveVisibleTools(args) {
|
|
230
|
+
const { permits, authHeader, registry, cache, analytics, collector, tenantId = "anonymous" } = args;
|
|
231
|
+
const t0 = Date.now();
|
|
232
|
+
const cacheKey = toolsCacheKey(permits ? authHeader : null);
|
|
233
|
+
const cached = await cache.get(cacheKey).catch(() => null);
|
|
234
|
+
if (cached) {
|
|
235
|
+
const durationMs2 = Date.now() - t0;
|
|
236
|
+
collector?.recordOp("mcp.tools.list", durationMs2, true);
|
|
237
|
+
analytics?.track("mcp.tools.list", {
|
|
238
|
+
tenantId,
|
|
239
|
+
toolCount: cached.length,
|
|
240
|
+
cached: true,
|
|
241
|
+
durationMs: durationMs2
|
|
242
|
+
}).catch(() => {
|
|
243
|
+
});
|
|
244
|
+
return cached;
|
|
245
|
+
}
|
|
246
|
+
const tools = permits ? filterTools(registry.snapshot(), permits) : [];
|
|
247
|
+
await cache.set(cacheKey, tools, TOOLS_CACHE_TTL_MS).catch(() => {
|
|
248
|
+
});
|
|
249
|
+
const durationMs = Date.now() - t0;
|
|
250
|
+
collector?.recordOp("mcp.tools.list", durationMs, true);
|
|
251
|
+
analytics?.track("mcp.tools.list", {
|
|
252
|
+
tenantId,
|
|
253
|
+
toolCount: tools.length,
|
|
254
|
+
cached: false,
|
|
255
|
+
durationMs
|
|
256
|
+
}).catch(() => {
|
|
257
|
+
});
|
|
258
|
+
return tools;
|
|
259
|
+
}
|
|
260
|
+
async function executeToolCall(args) {
|
|
261
|
+
const { name, visibleTools, permits, tenantId, resolvePlatform, analytics, collector } = args;
|
|
262
|
+
const t0 = Date.now();
|
|
263
|
+
const tool = visibleTools.find((t) => t.name === name);
|
|
264
|
+
if (!tool) {
|
|
265
|
+
return textResult(`Unknown tool: ${name}`, true);
|
|
266
|
+
}
|
|
267
|
+
if (!permits || !permits(tool.operationType, tool.pluginId)) {
|
|
268
|
+
return textResult(`Not authorized: ${name}`, true);
|
|
269
|
+
}
|
|
270
|
+
analytics?.track("mcp.tool.call.started", {
|
|
271
|
+
toolName: name,
|
|
272
|
+
tenantId,
|
|
273
|
+
pluginId: tool.pluginId
|
|
274
|
+
}).catch(() => {
|
|
275
|
+
});
|
|
276
|
+
const result = await callTool(tool, args.args, tenantId, resolvePlatform);
|
|
277
|
+
const durationMs = Date.now() - t0;
|
|
278
|
+
collector?.recordOp("mcp.tool.call", durationMs, result.success);
|
|
279
|
+
analytics?.track("mcp.tool.call.completed", {
|
|
280
|
+
toolName: name,
|
|
281
|
+
tenantId,
|
|
282
|
+
pluginId: tool.pluginId,
|
|
283
|
+
success: result.success,
|
|
284
|
+
exitCode: result.exitCode,
|
|
285
|
+
outputLength: result.output.length,
|
|
286
|
+
durationMs
|
|
287
|
+
}).catch(() => {
|
|
288
|
+
});
|
|
289
|
+
return textResult(result.output, !result.success);
|
|
290
|
+
}
|
|
291
|
+
var McpObservabilityCollector = class {
|
|
292
|
+
instanceId = resolveObservabilityInstanceId();
|
|
293
|
+
startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
294
|
+
activeRequests = 0;
|
|
295
|
+
requestsTotal = 0;
|
|
296
|
+
errorsTotal = 0;
|
|
297
|
+
ops = new OperationMetricsTracker();
|
|
298
|
+
/**
|
|
299
|
+
* Register Fastify hooks that track HTTP-level request counts and duration.
|
|
300
|
+
* Must be called before routes are registered so the hooks apply to all routes.
|
|
301
|
+
*/
|
|
302
|
+
register(server) {
|
|
303
|
+
server.addHook("onRequest", (req, _reply, done) => {
|
|
304
|
+
req.kbMetricsStart = performance.now();
|
|
305
|
+
this.activeRequests++;
|
|
306
|
+
done();
|
|
307
|
+
});
|
|
308
|
+
server.addHook("onResponse", (req, reply, done) => {
|
|
309
|
+
const start = req.kbMetricsStart;
|
|
310
|
+
const durationMs = start != null ? performance.now() - start : 0;
|
|
311
|
+
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
312
|
+
this.requestsTotal++;
|
|
313
|
+
if (reply.statusCode >= 400) {
|
|
314
|
+
this.errorsTotal++;
|
|
315
|
+
}
|
|
316
|
+
const route = (req.routeOptions?.url ?? req.url).replace(/[?#].*$/, "");
|
|
317
|
+
this.ops.recordOperation(
|
|
318
|
+
`http.${req.method} ${route}`,
|
|
319
|
+
durationMs,
|
|
320
|
+
reply.statusCode >= 400 ? "error" : "ok"
|
|
321
|
+
);
|
|
322
|
+
done();
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Record a completed domain operation. Called from request-handler after
|
|
327
|
+
* each tools/list and tools/call cycle.
|
|
328
|
+
*/
|
|
329
|
+
recordOp(name, durationMs, ok) {
|
|
330
|
+
this.ops.recordOperation(name, durationMs, ok ? "ok" : "error");
|
|
331
|
+
}
|
|
332
|
+
// ── Observability payload builders ──────────────────────────────────────
|
|
333
|
+
buildDescribe(registryReady, toolCount) {
|
|
334
|
+
return createServiceObservabilityDescribe({
|
|
335
|
+
schema: "kb.observability/1",
|
|
336
|
+
contractVersion: "1.0",
|
|
337
|
+
serviceId: "mcp-daemon",
|
|
338
|
+
instanceId: this.instanceId,
|
|
339
|
+
serviceType: "mcp-server",
|
|
340
|
+
version: process.env.npm_package_version ?? "0.0.0",
|
|
341
|
+
environment: process.env.NODE_ENV ?? "development",
|
|
342
|
+
startedAt: this.startedAt,
|
|
343
|
+
logsSource: "mcp-daemon",
|
|
344
|
+
dependencies: [],
|
|
345
|
+
metricsEndpoint: "/metrics",
|
|
346
|
+
healthEndpoint: "/observability/health",
|
|
347
|
+
capabilities: ["httpMetrics", "operationMetrics", "logCorrelation"],
|
|
348
|
+
metricFamilies: [
|
|
349
|
+
"process_rss_bytes",
|
|
350
|
+
"process_heap_used_bytes",
|
|
351
|
+
"service_active_operations",
|
|
352
|
+
"http_requests_total",
|
|
353
|
+
"http_errors_total",
|
|
354
|
+
"service_operation_total",
|
|
355
|
+
"service_operation_duration_ms"
|
|
356
|
+
],
|
|
357
|
+
meta: { toolCount, registryReady }
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
buildHealth(registryReady, toolCount, executionMode) {
|
|
361
|
+
const mem = process.memoryUsage();
|
|
362
|
+
return createServiceObservabilityHealth({
|
|
363
|
+
schema: "kb.observability/1",
|
|
364
|
+
contractVersion: "1.0",
|
|
365
|
+
serviceId: "mcp-daemon",
|
|
366
|
+
instanceId: this.instanceId,
|
|
367
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
368
|
+
status: registryReady ? "healthy" : "degraded",
|
|
369
|
+
uptimeSec: Math.floor(process.uptime()),
|
|
370
|
+
logsSource: "mcp-daemon",
|
|
371
|
+
metricsEndpoint: "/metrics",
|
|
372
|
+
capabilities: ["httpMetrics", "operationMetrics", "logCorrelation"],
|
|
373
|
+
snapshot: {
|
|
374
|
+
rssBytes: mem.rss,
|
|
375
|
+
heapUsedBytes: mem.heapUsed,
|
|
376
|
+
activeOperations: this.activeRequests
|
|
377
|
+
},
|
|
378
|
+
checks: [
|
|
379
|
+
{
|
|
380
|
+
id: "registry",
|
|
381
|
+
status: registryReady ? "ok" : "warn",
|
|
382
|
+
message: registryReady ? `${toolCount} tools loaded` : "Registry not yet ready"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: "execution",
|
|
386
|
+
status: "ok",
|
|
387
|
+
message: `mode=${executionMode}`
|
|
388
|
+
}
|
|
389
|
+
],
|
|
390
|
+
topOperations: this.ops.getTopOperations(5)
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
renderPrometheusMetrics(toolCount) {
|
|
394
|
+
const mem = process.memoryUsage();
|
|
395
|
+
const lines = [
|
|
396
|
+
"# HELP process_rss_bytes RSS memory in bytes",
|
|
397
|
+
`process_rss_bytes ${mem.rss}`,
|
|
398
|
+
"# HELP process_heap_used_bytes Heap used in bytes",
|
|
399
|
+
`process_heap_used_bytes ${mem.heapUsed}`,
|
|
400
|
+
"# HELP process_uptime_seconds Process uptime in seconds",
|
|
401
|
+
`process_uptime_seconds ${Math.floor(process.uptime())}`,
|
|
402
|
+
"# HELP mcp_tools_total Number of tools registered in the current snapshot",
|
|
403
|
+
`mcp_tools_total ${toolCount}`,
|
|
404
|
+
"# HELP http_requests_total Total MCP HTTP requests received",
|
|
405
|
+
`http_requests_total ${this.requestsTotal}`,
|
|
406
|
+
"# HELP http_errors_total Total MCP HTTP 4xx/5xx responses",
|
|
407
|
+
`http_errors_total ${this.errorsTotal}`,
|
|
408
|
+
"# HELP service_active_operations Currently active MCP requests",
|
|
409
|
+
`service_active_operations ${this.activeRequests}`,
|
|
410
|
+
...this.ops.getMetricLines()
|
|
411
|
+
];
|
|
412
|
+
return lines.join("\n");
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// src/server.ts
|
|
417
|
+
var McpDaemonServer = class {
|
|
418
|
+
opts;
|
|
419
|
+
resolvePlatform;
|
|
420
|
+
collector;
|
|
421
|
+
app;
|
|
422
|
+
registry;
|
|
423
|
+
constructor(opts) {
|
|
424
|
+
this.opts = opts;
|
|
425
|
+
this.resolvePlatform = opts.resolvePlatform ?? (() => opts.platform);
|
|
426
|
+
this.collector = new McpObservabilityCollector();
|
|
427
|
+
}
|
|
428
|
+
/** Build a per-request permits predicate bound to the identity (PDP seam). */
|
|
429
|
+
permitsFor(identity) {
|
|
430
|
+
return (operationType, resource) => can(this.opts.policy, identity, actionForOperation(operationType), resource);
|
|
431
|
+
}
|
|
432
|
+
get toolCount() {
|
|
433
|
+
return this.registry?.snapshot().manifests.flatMap((m) => m.manifest.cli?.commands ?? []).length ?? 0;
|
|
434
|
+
}
|
|
435
|
+
async start() {
|
|
436
|
+
this.registry = await createToolRegistry({
|
|
437
|
+
root: this.opts.projectRoot,
|
|
438
|
+
platformRoot: this.opts.platformRoot,
|
|
439
|
+
cache: this.opts.cache
|
|
440
|
+
});
|
|
441
|
+
const execMode = process.env.KB_MCP_EXECUTION_MODE ?? "subprocess";
|
|
442
|
+
const observabilityAdapter = {
|
|
443
|
+
register: (server) => this.collector.register(server),
|
|
444
|
+
buildDescribe: () => this.collector.buildDescribe(!!this.registry, this.toolCount),
|
|
445
|
+
buildHealth: () => this.collector.buildHealth(!!this.registry, this.toolCount, execMode),
|
|
446
|
+
renderPrometheusMetrics: (_status) => this.collector.renderPrometheusMetrics(this.toolCount)
|
|
447
|
+
};
|
|
448
|
+
this.app = await createDaemonServer({
|
|
449
|
+
serviceId: "mcp-daemon",
|
|
450
|
+
logger: this.opts.logger,
|
|
451
|
+
observability: observabilityAdapter,
|
|
452
|
+
readyCheck: () => ({ ready: !!this.registry, service: "mcp-daemon" }),
|
|
453
|
+
registerRoutes: async (server) => this.registerMcpRoutes(server)
|
|
454
|
+
});
|
|
455
|
+
const listenOptions = getListenOptions(this.opts.port, this.opts.host);
|
|
456
|
+
const address = await this.app.listen(listenOptions);
|
|
457
|
+
this.opts.logger.info("MCP daemon listening", { address });
|
|
458
|
+
return address;
|
|
459
|
+
}
|
|
460
|
+
async stop() {
|
|
461
|
+
await this.app?.close();
|
|
462
|
+
}
|
|
463
|
+
async registerMcpRoutes(server) {
|
|
464
|
+
const { cache, logger, jwtConfig } = this.opts;
|
|
465
|
+
server.all("/api/v1/mcp", async (request, reply) => {
|
|
466
|
+
reply.header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Methods", "GET, POST, OPTIONS").header("Access-Control-Allow-Headers", "Authorization, Content-Type, Mcp-Session-Id");
|
|
467
|
+
if (request.method === "OPTIONS") {
|
|
468
|
+
return reply.code(204).send();
|
|
469
|
+
}
|
|
470
|
+
const reqLogger = request.kbLogger ?? logger;
|
|
471
|
+
const authHeader = request.headers.authorization;
|
|
472
|
+
const authCtx = await resolveAuthContext(authHeader, cache, jwtConfig).catch(
|
|
473
|
+
() => null
|
|
474
|
+
);
|
|
475
|
+
const tenantId = authCtx?.namespaceId ?? "anonymous";
|
|
476
|
+
const permits = authCtx ? this.permitsFor(toIdentity(authCtx)) : null;
|
|
477
|
+
const visibleTools = await resolveVisibleTools({
|
|
478
|
+
permits,
|
|
479
|
+
authHeader,
|
|
480
|
+
registry: this.registry,
|
|
481
|
+
cache,
|
|
482
|
+
analytics: this.opts.platform.analytics,
|
|
483
|
+
collector: this.collector,
|
|
484
|
+
tenantId
|
|
485
|
+
});
|
|
486
|
+
const mcp = new Server(
|
|
487
|
+
{ name: "kb-labs", version: "1.0.0" },
|
|
488
|
+
{ capabilities: { tools: {} } }
|
|
489
|
+
);
|
|
490
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
491
|
+
tools: visibleTools.map((t) => ({
|
|
492
|
+
name: t.name,
|
|
493
|
+
description: t.description,
|
|
494
|
+
inputSchema: t.inputSchema
|
|
495
|
+
}))
|
|
496
|
+
}));
|
|
497
|
+
mcp.setRequestHandler(
|
|
498
|
+
CallToolRequestSchema,
|
|
499
|
+
async ({ params }) => executeToolCall({
|
|
500
|
+
name: params.name,
|
|
501
|
+
args: params.arguments ?? {},
|
|
502
|
+
visibleTools,
|
|
503
|
+
permits,
|
|
504
|
+
tenantId,
|
|
505
|
+
resolvePlatform: this.resolvePlatform,
|
|
506
|
+
analytics: this.opts.platform.analytics,
|
|
507
|
+
collector: this.collector
|
|
508
|
+
})
|
|
509
|
+
);
|
|
510
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
511
|
+
try {
|
|
512
|
+
await mcp.connect(transport);
|
|
513
|
+
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
reqLogger.error("MCP request handling failed", error);
|
|
516
|
+
if (!reply.raw.headersSent) {
|
|
517
|
+
reply.code(500).send({ error: "internal_error" });
|
|
518
|
+
}
|
|
519
|
+
} finally {
|
|
520
|
+
await mcp.close().catch(() => {
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/bootstrap.ts
|
|
528
|
+
async function bootstrap() {
|
|
529
|
+
await runDaemon(
|
|
530
|
+
{
|
|
531
|
+
appId: "mcp-daemon",
|
|
532
|
+
defaultPort: 7779,
|
|
533
|
+
portEnvVar: "KB_MCP_DAEMON_PORT",
|
|
534
|
+
defaultHost: "localhost",
|
|
535
|
+
hostEnvVar: "KB_MCP_DAEMON_HOST",
|
|
536
|
+
async setup({ port, host }) {
|
|
537
|
+
const logger = createCorrelatedLogger(platform.logger, {
|
|
538
|
+
serviceId: "mcp-daemon",
|
|
539
|
+
instanceId: resolveObservabilityInstanceId(),
|
|
540
|
+
logsSource: "mcp-daemon",
|
|
541
|
+
layer: "mcp",
|
|
542
|
+
service: "bootstrap",
|
|
543
|
+
operation: "mcp-daemon.bootstrap"
|
|
544
|
+
});
|
|
545
|
+
logger.info("MCP daemon platform initialised");
|
|
546
|
+
const projectRoot = getProjectRoot() ?? process.cwd();
|
|
547
|
+
const platformRoot = getPlatformRoot();
|
|
548
|
+
const { policy } = await resolvePolicy({});
|
|
549
|
+
const server = new McpDaemonServer({
|
|
550
|
+
port,
|
|
551
|
+
host,
|
|
552
|
+
logger,
|
|
553
|
+
cache: platform.cache,
|
|
554
|
+
platform,
|
|
555
|
+
resolvePlatform: () => platform,
|
|
556
|
+
projectRoot,
|
|
557
|
+
platformRoot,
|
|
558
|
+
jwtConfig: loadJwtConfig(),
|
|
559
|
+
policy
|
|
560
|
+
});
|
|
561
|
+
await server.start();
|
|
562
|
+
return async () => {
|
|
563
|
+
await server.stop();
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
// platformBootstrap: full init with uiProvider for callOutput ALS wiring.
|
|
568
|
+
// uiProvider flows into initPlatform → createExecutionBackend so the backend
|
|
569
|
+
// captures plugin output from the very start. Concurrent tool calls are
|
|
570
|
+
// fully isolated — each gets its own AsyncLocalStorage slot.
|
|
571
|
+
async (appId, repoRoot) => {
|
|
572
|
+
await createServiceBootstrap({
|
|
573
|
+
appId,
|
|
574
|
+
repoRoot,
|
|
575
|
+
uiProvider: (_hostType) => {
|
|
576
|
+
const lines = callOutput.getStore();
|
|
577
|
+
return lines ? createBufferedUI((s) => lines.push(s)).ui : noopUI;
|
|
578
|
+
},
|
|
579
|
+
assemblyHook: makeAssemblyHook()
|
|
580
|
+
});
|
|
581
|
+
return platform;
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/index.ts
|
|
587
|
+
bootstrap().catch((error) => {
|
|
588
|
+
console.error("Failed to start MCP daemon:", error);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
});
|
|
591
|
+
//# sourceMappingURL=index.js.map
|
|
592
|
+
//# sourceMappingURL=index.js.map
|