pumuki 6.3.26 → 6.3.28
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 +3 -1
- package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
- package/bin/pumuki-mcp-evidence-stdio.js +5 -0
- package/core/gate/conditionMatches.ts +1 -21
- package/core/gate/evaluateGate.js +5 -0
- package/core/gate/evaluateRules.js +5 -0
- package/core/gate/evaluateRules.ts +1 -24
- package/core/gate/scopeMatcher.ts +84 -0
- package/docs/EXECUTION_BOARD.md +749 -376
- package/docs/MCP_SERVERS.md +41 -2
- package/docs/README.md +6 -2
- package/docs/REFRACTOR_PROGRESS.md +374 -6
- package/docs/validation/README.md +11 -1
- package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
- package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
- package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
- package/integrations/config/skillsCompliance.ts +212 -0
- package/integrations/evidence/integrity.ts +352 -0
- package/integrations/evidence/rulesCoverage.ts +94 -0
- package/integrations/evidence/schema.test.ts +16 -0
- package/integrations/evidence/schema.ts +41 -0
- package/integrations/evidence/writeEvidence.test.ts +68 -0
- package/integrations/evidence/writeEvidence.ts +23 -2
- package/integrations/gate/evaluateAiGate.ts +382 -15
- package/integrations/gate/stagePolicies.ts +70 -15
- package/integrations/gate/waivers.ts +209 -0
- package/integrations/git/findingTraceability.ts +3 -23
- package/integrations/git/index.js +5 -0
- package/integrations/git/runCliCommand.ts +16 -0
- package/integrations/git/runPlatformGate.ts +53 -1
- package/integrations/git/runPlatformGateEvaluation.ts +13 -0
- package/integrations/git/stageRunners.ts +168 -5
- package/integrations/lifecycle/adapter.templates.json +72 -5
- package/integrations/lifecycle/adapter.ts +78 -4
- package/integrations/lifecycle/cli.ts +384 -14
- package/integrations/lifecycle/doctor.ts +534 -0
- package/integrations/lifecycle/hookBlock.ts +2 -1
- package/integrations/lifecycle/index.js +5 -0
- package/integrations/lifecycle/install.ts +115 -3
- package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
- package/integrations/lifecycle/preWriteAutomation.ts +142 -0
- package/integrations/mcp/aiGateCheck.ts +6 -0
- package/integrations/mcp/aiGateReceipt.ts +188 -0
- package/integrations/mcp/enterpriseServer.ts +14 -1
- package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
- package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
- package/integrations/mcp/index.js +5 -0
- package/integrations/sdd/index.js +5 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/policy.ts +191 -2
- package/integrations/sdd/sessionStore.ts +139 -19
- package/integrations/sdd/syncDocs.ts +180 -0
- package/integrations/sdd/types.ts +4 -1
- package/integrations/telemetry/structuredTelemetry.ts +197 -0
- package/package.json +27 -8
- package/scripts/build-p9-validation-manifests.ts +53 -0
- package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
- package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
- package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
- package/scripts/check-p9-ruralgo-install-health.ts +288 -0
- package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
- package/scripts/check-package-manifest.ts +49 -0
- package/scripts/check-tracking-single-active.sh +40 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
- package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
- package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
- package/scripts/manage-library.sh +1 -1
- package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
- package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
- package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
- package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
- package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
- package/scripts/p9-validation-manifests-lib.ts +366 -0
- package/scripts/package-manifest-lib.ts +9 -0
- package/skills.lock.json +1 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { Socket, createServer } from 'node:net';
|
|
2
|
+
import { startEnterpriseMcpServer } from './enterpriseServer';
|
|
3
|
+
|
|
4
|
+
type JsonRpcId = string | number | null;
|
|
5
|
+
|
|
6
|
+
type JsonRpcRequest = {
|
|
7
|
+
jsonrpc?: unknown;
|
|
8
|
+
id?: unknown;
|
|
9
|
+
method?: unknown;
|
|
10
|
+
params?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type JsonRpcResponse = {
|
|
14
|
+
jsonrpc: '2.0';
|
|
15
|
+
id: JsonRpcId;
|
|
16
|
+
result?: unknown;
|
|
17
|
+
error?: {
|
|
18
|
+
code: number;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
24
|
+
|
|
25
|
+
const toJsonRpcId = (value: unknown): JsonRpcId => {
|
|
26
|
+
if (typeof value === 'string' || typeof value === 'number' || value === null) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sendMessage = (message: JsonRpcResponse): void => {
|
|
33
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const sendResult = (id: JsonRpcId, result: unknown): void => {
|
|
37
|
+
sendMessage({
|
|
38
|
+
jsonrpc: '2.0',
|
|
39
|
+
id,
|
|
40
|
+
result,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const sendError = (id: JsonRpcId, code: number, message: string): void => {
|
|
45
|
+
sendMessage({
|
|
46
|
+
jsonrpc: '2.0',
|
|
47
|
+
id,
|
|
48
|
+
error: {
|
|
49
|
+
code,
|
|
50
|
+
message,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isPortInUse = async (host: string, port: number): Promise<boolean> =>
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
const socket = new Socket();
|
|
58
|
+
socket.setTimeout(600);
|
|
59
|
+
socket.once('connect', () => {
|
|
60
|
+
socket.destroy();
|
|
61
|
+
resolve(true);
|
|
62
|
+
});
|
|
63
|
+
socket.once('timeout', () => {
|
|
64
|
+
socket.destroy();
|
|
65
|
+
resolve(false);
|
|
66
|
+
});
|
|
67
|
+
socket.once('error', () => {
|
|
68
|
+
socket.destroy();
|
|
69
|
+
resolve(false);
|
|
70
|
+
});
|
|
71
|
+
socket.connect(port, host);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const findEphemeralPort = async (host: string): Promise<number> =>
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
const probe = createServer();
|
|
77
|
+
probe.once('error', reject);
|
|
78
|
+
probe.listen(0, host, () => {
|
|
79
|
+
const address = probe.address();
|
|
80
|
+
const port = address && typeof address === 'object' ? address.port : 0;
|
|
81
|
+
probe.close(() => resolve(port));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const fetchJson = async (url: string, options?: RequestInit): Promise<unknown> => {
|
|
86
|
+
const response = await fetch(url, options);
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
if (text.trim().length === 0) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(text) as unknown;
|
|
93
|
+
} catch {
|
|
94
|
+
return {
|
|
95
|
+
status: response.status,
|
|
96
|
+
body: text,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const startOrReuseEnterpriseHttp = async (): Promise<{
|
|
102
|
+
host: string;
|
|
103
|
+
port: number;
|
|
104
|
+
startedByThisProcess: boolean;
|
|
105
|
+
}> => {
|
|
106
|
+
const host = process.env.PUMUKI_ENTERPRISE_MCP_HOST ?? '127.0.0.1';
|
|
107
|
+
const parsedPort = Number.parseInt(process.env.PUMUKI_ENTERPRISE_MCP_PORT ?? '', 10);
|
|
108
|
+
const preferredPort = Number.isFinite(parsedPort) ? parsedPort : 7391;
|
|
109
|
+
const requestedPort = preferredPort > 0 ? preferredPort : await findEphemeralPort(host);
|
|
110
|
+
|
|
111
|
+
const healthUrl = `http://${host}:${requestedPort}/health`;
|
|
112
|
+
try {
|
|
113
|
+
const health = (await fetchJson(healthUrl)) as { status?: string };
|
|
114
|
+
if (health.status === 'ok') {
|
|
115
|
+
return {
|
|
116
|
+
host,
|
|
117
|
+
port: requestedPort,
|
|
118
|
+
startedByThisProcess: false,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Intentionally ignored: endpoint not available yet.
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const portInUse = await isPortInUse(host, requestedPort);
|
|
126
|
+
const resolvedPort = portInUse ? await findEphemeralPort(host) : requestedPort;
|
|
127
|
+
startEnterpriseMcpServer({
|
|
128
|
+
host,
|
|
129
|
+
port: resolvedPort,
|
|
130
|
+
repoRoot: process.cwd(),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
host,
|
|
135
|
+
port: resolvedPort,
|
|
136
|
+
startedByThisProcess: true,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const toToolInputSchema = (): Record<string, unknown> => ({
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {},
|
|
143
|
+
additionalProperties: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const run = async (): Promise<void> => {
|
|
147
|
+
const httpServer = await startOrReuseEnterpriseHttp();
|
|
148
|
+
const baseUrl = `http://${httpServer.host}:${httpServer.port}`;
|
|
149
|
+
let textBuffer = '';
|
|
150
|
+
|
|
151
|
+
const handleRequest = async (request: JsonRpcRequest): Promise<void> => {
|
|
152
|
+
if (request.jsonrpc !== '2.0') {
|
|
153
|
+
sendError(toJsonRpcId(request.id), -32600, 'Invalid JSON-RPC version.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const id = toJsonRpcId(request.id);
|
|
158
|
+
const method = typeof request.method === 'string' ? request.method : '';
|
|
159
|
+
const params = request.params;
|
|
160
|
+
|
|
161
|
+
if (method === 'notifications/initialized') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (method === 'initialize') {
|
|
166
|
+
sendResult(id, {
|
|
167
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
168
|
+
serverInfo: {
|
|
169
|
+
name: 'pumuki-enterprise-stdio',
|
|
170
|
+
version: '1.0.0',
|
|
171
|
+
},
|
|
172
|
+
capabilities: {
|
|
173
|
+
tools: {
|
|
174
|
+
listChanged: true,
|
|
175
|
+
},
|
|
176
|
+
resources: {
|
|
177
|
+
listChanged: true,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (method === 'ping') {
|
|
185
|
+
sendResult(id, {});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (method === 'tools/list') {
|
|
190
|
+
const payload = (await fetchJson(`${baseUrl}/tools`)) as {
|
|
191
|
+
tools?: Array<{ name?: string; description?: string }>;
|
|
192
|
+
};
|
|
193
|
+
const tools = (payload.tools ?? [])
|
|
194
|
+
.filter((entry) => typeof entry?.name === 'string')
|
|
195
|
+
.map((entry) => ({
|
|
196
|
+
name: entry.name as string,
|
|
197
|
+
description: typeof entry.description === 'string' ? entry.description : undefined,
|
|
198
|
+
inputSchema: toToolInputSchema(),
|
|
199
|
+
}));
|
|
200
|
+
sendResult(id, {
|
|
201
|
+
tools,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (method === 'tools/call') {
|
|
207
|
+
const callParams = typeof params === 'object' && params !== null
|
|
208
|
+
? (params as { name?: unknown; arguments?: unknown })
|
|
209
|
+
: {};
|
|
210
|
+
const name = typeof callParams.name === 'string' ? callParams.name : '';
|
|
211
|
+
const args = typeof callParams.arguments === 'object' && callParams.arguments !== null
|
|
212
|
+
? (callParams.arguments as Record<string, unknown>)
|
|
213
|
+
: {};
|
|
214
|
+
const payload = await fetchJson(`${baseUrl}/tool`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
'Content-Type': 'application/json',
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
name,
|
|
221
|
+
args,
|
|
222
|
+
dryRun: true,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
const envelope = payload as { success?: boolean };
|
|
226
|
+
sendResult(id, {
|
|
227
|
+
content: [
|
|
228
|
+
{
|
|
229
|
+
type: 'text',
|
|
230
|
+
text: JSON.stringify(payload),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
isError: envelope.success === false,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (method === 'resources/list') {
|
|
239
|
+
const payload = (await fetchJson(`${baseUrl}/resources`)) as {
|
|
240
|
+
resources?: Array<{ uri?: string; name?: string; description?: string }>;
|
|
241
|
+
};
|
|
242
|
+
const resources = (payload.resources ?? [])
|
|
243
|
+
.filter((entry) => typeof entry?.uri === 'string')
|
|
244
|
+
.map((entry) => ({
|
|
245
|
+
uri: entry.uri as string,
|
|
246
|
+
name: typeof entry.name === 'string' ? entry.name : entry.uri,
|
|
247
|
+
description: typeof entry.description === 'string' ? entry.description : undefined,
|
|
248
|
+
mimeType: 'application/json',
|
|
249
|
+
}));
|
|
250
|
+
sendResult(id, {
|
|
251
|
+
resources,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (method === 'resources/read') {
|
|
257
|
+
const readParams = typeof params === 'object' && params !== null
|
|
258
|
+
? (params as { uri?: unknown })
|
|
259
|
+
: {};
|
|
260
|
+
const uri = typeof readParams.uri === 'string' ? readParams.uri : '';
|
|
261
|
+
const payload = await fetchJson(
|
|
262
|
+
`${baseUrl}/resource?uri=${encodeURIComponent(uri)}`
|
|
263
|
+
);
|
|
264
|
+
sendResult(id, {
|
|
265
|
+
contents: [
|
|
266
|
+
{
|
|
267
|
+
uri,
|
|
268
|
+
mimeType: 'application/json',
|
|
269
|
+
text: JSON.stringify(payload),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const processBuffer = (): void => {
|
|
280
|
+
while (true) {
|
|
281
|
+
const lineEnd = textBuffer.indexOf('\n');
|
|
282
|
+
if (lineEnd === -1) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const rawLine = textBuffer.slice(0, lineEnd).trim();
|
|
286
|
+
textBuffer = textBuffer.slice(lineEnd + 1);
|
|
287
|
+
if (rawLine.length === 0) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
let payload: JsonRpcRequest;
|
|
291
|
+
try {
|
|
292
|
+
payload = JSON.parse(rawLine) as JsonRpcRequest;
|
|
293
|
+
} catch {
|
|
294
|
+
sendError(null, -32700, 'Parse error');
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
void handleRequest(payload).catch((error) => {
|
|
298
|
+
const id = toJsonRpcId(payload.id);
|
|
299
|
+
const message = error instanceof Error ? error.message : 'Internal error';
|
|
300
|
+
sendError(id, -32603, message);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
process.stdin.on('data', (chunk) => {
|
|
306
|
+
textBuffer += chunk.toString('utf8');
|
|
307
|
+
processBuffer();
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
void run().catch((error) => {
|
|
312
|
+
const message = error instanceof Error ? error.message : 'Unknown MCP stdio bridge error';
|
|
313
|
+
process.stderr.write(`[pumuki-mcp-enterprise-stdio] ${message}\n`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { Socket, createServer } from 'node:net';
|
|
2
|
+
import { startEvidenceContextServer } from './evidenceContextServer';
|
|
3
|
+
|
|
4
|
+
type JsonRpcId = string | number | null;
|
|
5
|
+
|
|
6
|
+
type JsonRpcRequest = {
|
|
7
|
+
jsonrpc?: unknown;
|
|
8
|
+
id?: unknown;
|
|
9
|
+
method?: unknown;
|
|
10
|
+
params?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type JsonRpcResponse = {
|
|
14
|
+
jsonrpc: '2.0';
|
|
15
|
+
id: JsonRpcId;
|
|
16
|
+
result?: unknown;
|
|
17
|
+
error?: {
|
|
18
|
+
code: number;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
24
|
+
|
|
25
|
+
const toJsonRpcId = (value: unknown): JsonRpcId => {
|
|
26
|
+
if (typeof value === 'string' || typeof value === 'number' || value === null) {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sendMessage = (message: JsonRpcResponse): void => {
|
|
33
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const sendResult = (id: JsonRpcId, result: unknown): void => {
|
|
37
|
+
sendMessage({
|
|
38
|
+
jsonrpc: '2.0',
|
|
39
|
+
id,
|
|
40
|
+
result,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const sendError = (id: JsonRpcId, code: number, message: string): void => {
|
|
45
|
+
sendMessage({
|
|
46
|
+
jsonrpc: '2.0',
|
|
47
|
+
id,
|
|
48
|
+
error: {
|
|
49
|
+
code,
|
|
50
|
+
message,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isPortInUse = async (host: string, port: number): Promise<boolean> =>
|
|
56
|
+
await new Promise((resolve) => {
|
|
57
|
+
const socket = new Socket();
|
|
58
|
+
socket.setTimeout(600);
|
|
59
|
+
socket.once('connect', () => {
|
|
60
|
+
socket.destroy();
|
|
61
|
+
resolve(true);
|
|
62
|
+
});
|
|
63
|
+
socket.once('timeout', () => {
|
|
64
|
+
socket.destroy();
|
|
65
|
+
resolve(false);
|
|
66
|
+
});
|
|
67
|
+
socket.once('error', () => {
|
|
68
|
+
socket.destroy();
|
|
69
|
+
resolve(false);
|
|
70
|
+
});
|
|
71
|
+
socket.connect(port, host);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const findEphemeralPort = async (host: string): Promise<number> =>
|
|
75
|
+
await new Promise((resolve, reject) => {
|
|
76
|
+
const probe = createServer();
|
|
77
|
+
probe.once('error', reject);
|
|
78
|
+
probe.listen(0, host, () => {
|
|
79
|
+
const address = probe.address();
|
|
80
|
+
const port = address && typeof address === 'object' ? address.port : 0;
|
|
81
|
+
probe.close(() => resolve(port));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const fetchJson = async (url: string): Promise<unknown> => {
|
|
86
|
+
const response = await fetch(url);
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
if (text.trim().length === 0) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
return JSON.parse(text) as unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const startOrReuseEvidenceHttp = async (): Promise<{
|
|
95
|
+
host: string;
|
|
96
|
+
port: number;
|
|
97
|
+
route: string;
|
|
98
|
+
}> => {
|
|
99
|
+
const host = process.env.PUMUKI_EVIDENCE_HOST ?? '127.0.0.1';
|
|
100
|
+
const route = process.env.PUMUKI_EVIDENCE_ROUTE ?? '/ai-evidence';
|
|
101
|
+
const parsedPort = Number.parseInt(process.env.PUMUKI_EVIDENCE_PORT ?? '', 10);
|
|
102
|
+
const preferredPort = Number.isFinite(parsedPort) ? parsedPort : 7341;
|
|
103
|
+
const requestedPort = preferredPort > 0 ? preferredPort : await findEphemeralPort(host);
|
|
104
|
+
const healthUrl = `http://${host}:${requestedPort}/health`;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const health = (await fetchJson(healthUrl)) as { status?: string };
|
|
108
|
+
if (health.status === 'ok') {
|
|
109
|
+
return { host, port: requestedPort, route };
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// ignored
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const portInUse = await isPortInUse(host, requestedPort);
|
|
116
|
+
const resolvedPort = portInUse ? await findEphemeralPort(host) : requestedPort;
|
|
117
|
+
startEvidenceContextServer({
|
|
118
|
+
host,
|
|
119
|
+
port: resolvedPort,
|
|
120
|
+
route,
|
|
121
|
+
repoRoot: process.cwd(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
host,
|
|
126
|
+
port: resolvedPort,
|
|
127
|
+
route,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const run = async (): Promise<void> => {
|
|
132
|
+
const started = await startOrReuseEvidenceHttp();
|
|
133
|
+
const baseUrl = `http://${started.host}:${started.port}`;
|
|
134
|
+
const route = started.route.startsWith('/') ? started.route : `/${started.route}`;
|
|
135
|
+
let textBuffer = '';
|
|
136
|
+
|
|
137
|
+
const resourcesCatalog = [
|
|
138
|
+
{
|
|
139
|
+
uri: 'evidence://status',
|
|
140
|
+
path: '/status',
|
|
141
|
+
description: 'Evidence status payload',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
uri: 'evidence://summary',
|
|
145
|
+
path: `${route}/summary`,
|
|
146
|
+
description: 'Evidence summary payload',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
uri: 'evidence://snapshot',
|
|
150
|
+
path: `${route}/snapshot`,
|
|
151
|
+
description: 'Evidence snapshot payload',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
uri: 'evidence://findings',
|
|
155
|
+
path: `${route}/findings`,
|
|
156
|
+
description: 'Evidence findings payload',
|
|
157
|
+
},
|
|
158
|
+
] as const;
|
|
159
|
+
|
|
160
|
+
const toolsCatalog = [
|
|
161
|
+
{
|
|
162
|
+
name: 'evidence_status',
|
|
163
|
+
path: '/status',
|
|
164
|
+
description: 'Read evidence status payload.',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'evidence_summary',
|
|
168
|
+
path: `${route}/summary`,
|
|
169
|
+
description: 'Read evidence summary payload.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'evidence_snapshot',
|
|
173
|
+
path: `${route}/snapshot`,
|
|
174
|
+
description: 'Read evidence snapshot payload.',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'evidence_findings',
|
|
178
|
+
path: `${route}/findings`,
|
|
179
|
+
description: 'Read evidence findings payload.',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'evidence_rulesets',
|
|
183
|
+
path: `${route}/rulesets`,
|
|
184
|
+
description: 'Read evidence rulesets payload.',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'evidence_platforms',
|
|
188
|
+
path: `${route}/platforms`,
|
|
189
|
+
description: 'Read evidence platforms payload.',
|
|
190
|
+
},
|
|
191
|
+
] as const;
|
|
192
|
+
|
|
193
|
+
const handleRequest = async (request: JsonRpcRequest): Promise<void> => {
|
|
194
|
+
if (request.jsonrpc !== '2.0') {
|
|
195
|
+
sendError(toJsonRpcId(request.id), -32600, 'Invalid JSON-RPC version.');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const id = toJsonRpcId(request.id);
|
|
200
|
+
const method = typeof request.method === 'string' ? request.method : '';
|
|
201
|
+
|
|
202
|
+
if (method === 'notifications/initialized') {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (method === 'initialize') {
|
|
207
|
+
sendResult(id, {
|
|
208
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
209
|
+
serverInfo: {
|
|
210
|
+
name: 'pumuki-evidence-stdio',
|
|
211
|
+
version: '1.0.0',
|
|
212
|
+
},
|
|
213
|
+
capabilities: {
|
|
214
|
+
tools: {
|
|
215
|
+
listChanged: true,
|
|
216
|
+
},
|
|
217
|
+
resources: {
|
|
218
|
+
listChanged: true,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (method === 'ping') {
|
|
226
|
+
sendResult(id, {});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (method === 'tools/list') {
|
|
231
|
+
sendResult(id, {
|
|
232
|
+
tools: toolsCatalog.map((entry) => ({
|
|
233
|
+
name: entry.name,
|
|
234
|
+
description: entry.description,
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {},
|
|
238
|
+
additionalProperties: true,
|
|
239
|
+
},
|
|
240
|
+
})),
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (method === 'tools/call') {
|
|
246
|
+
const params = typeof request.params === 'object' && request.params !== null
|
|
247
|
+
? (request.params as { name?: unknown; arguments?: unknown })
|
|
248
|
+
: {};
|
|
249
|
+
const name = typeof params.name === 'string' ? params.name : '';
|
|
250
|
+
const tool = toolsCatalog.find((entry) => entry.name === name);
|
|
251
|
+
if (!tool) {
|
|
252
|
+
sendError(id, -32602, `Unknown tool: ${name}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const payload = await fetchJson(`${baseUrl}${tool.path}`);
|
|
256
|
+
sendResult(id, {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: 'text',
|
|
260
|
+
text: JSON.stringify(payload),
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
isError: false,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (method === 'resources/list') {
|
|
269
|
+
sendResult(id, {
|
|
270
|
+
resources: resourcesCatalog.map((entry) => ({
|
|
271
|
+
uri: entry.uri,
|
|
272
|
+
name: entry.uri,
|
|
273
|
+
description: entry.description,
|
|
274
|
+
mimeType: 'application/json',
|
|
275
|
+
})),
|
|
276
|
+
});
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (method === 'resources/read') {
|
|
281
|
+
const params = typeof request.params === 'object' && request.params !== null
|
|
282
|
+
? (request.params as { uri?: unknown })
|
|
283
|
+
: {};
|
|
284
|
+
const uri = typeof params.uri === 'string' ? params.uri : '';
|
|
285
|
+
const resource = resourcesCatalog.find((entry) => entry.uri === uri);
|
|
286
|
+
if (!resource) {
|
|
287
|
+
sendError(id, -32602, `Unknown resource URI: ${uri}`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const payload = await fetchJson(`${baseUrl}${resource.path}`);
|
|
291
|
+
sendResult(id, {
|
|
292
|
+
contents: [
|
|
293
|
+
{
|
|
294
|
+
uri,
|
|
295
|
+
mimeType: 'application/json',
|
|
296
|
+
text: JSON.stringify(payload),
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
sendError(id, -32601, `Method not found: ${method}`);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const processBuffer = (): void => {
|
|
307
|
+
while (true) {
|
|
308
|
+
const lineEnd = textBuffer.indexOf('\n');
|
|
309
|
+
if (lineEnd === -1) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const rawLine = textBuffer.slice(0, lineEnd).trim();
|
|
313
|
+
textBuffer = textBuffer.slice(lineEnd + 1);
|
|
314
|
+
if (rawLine.length === 0) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
let payload: JsonRpcRequest;
|
|
318
|
+
try {
|
|
319
|
+
payload = JSON.parse(rawLine) as JsonRpcRequest;
|
|
320
|
+
} catch {
|
|
321
|
+
sendError(null, -32700, 'Parse error');
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
void handleRequest(payload).catch((error) => {
|
|
325
|
+
const id = toJsonRpcId(payload.id);
|
|
326
|
+
const message = error instanceof Error ? error.message : 'Internal error';
|
|
327
|
+
sendError(id, -32603, message);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
process.stdin.on('data', (chunk) => {
|
|
333
|
+
textBuffer += chunk.toString('utf8');
|
|
334
|
+
processBuffer();
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
void run().catch((error) => {
|
|
339
|
+
const message = error instanceof Error ? error.message : 'Unknown MCP stdio bridge error';
|
|
340
|
+
process.stderr.write(`[pumuki-mcp-evidence-stdio] ${message}\n`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
@@ -9,3 +9,5 @@ export type {
|
|
|
9
9
|
} from './types';
|
|
10
10
|
export { evaluateSddPolicy, readSddStatus } from './policy';
|
|
11
11
|
export { closeSddSession, openSddSession, readSddSession, refreshSddSession } from './sessionStore';
|
|
12
|
+
export { resolveSddSyncDocsPath, runSddSyncDocs } from './syncDocs';
|
|
13
|
+
export type { SddSyncDocsResult, SddSyncDocsStageCheck } from './syncDocs';
|