@enactprotocol/mcp-server 2.2.1 → 2.2.4
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/index.js +327 -304
- package/package.json +4 -4
- package/src/index.ts +150 -15
- package/tests/secrets.test.ts +465 -0
- package/tests/trust-policy.test.ts +280 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enactprotocol/mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "MCP protocol server for Enact tool integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"dev:http": "bun run src/index.ts --http"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@enactprotocol/api": "2.2.
|
|
28
|
-
"@enactprotocol/execution": "2.2.
|
|
29
|
-
"@enactprotocol/shared": "2.2.
|
|
27
|
+
"@enactprotocol/api": "2.2.4",
|
|
28
|
+
"@enactprotocol/execution": "2.2.4",
|
|
29
|
+
"@enactprotocol/shared": "2.2.4",
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.10.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* @see https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { mkdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
18
19
|
import { type IncomingMessage, type ServerResponse, createServer } from "node:http";
|
|
@@ -23,8 +24,10 @@ import {
|
|
|
23
24
|
getToolInfo,
|
|
24
25
|
getToolVersion,
|
|
25
26
|
searchTools,
|
|
27
|
+
verifyAllAttestations,
|
|
26
28
|
} from "@enactprotocol/api";
|
|
27
29
|
import { DaggerExecutionProvider } from "@enactprotocol/execution";
|
|
30
|
+
import { resolveSecret } from "@enactprotocol/secrets";
|
|
28
31
|
import {
|
|
29
32
|
type ToolManifest,
|
|
30
33
|
addMcpTool,
|
|
@@ -33,7 +36,10 @@ import {
|
|
|
33
36
|
getActiveToolset,
|
|
34
37
|
getCacheDir,
|
|
35
38
|
getMcpToolInfo,
|
|
39
|
+
getMinimumAttestations,
|
|
36
40
|
getToolCachePath,
|
|
41
|
+
getTrustPolicy,
|
|
42
|
+
isIdentityTrusted,
|
|
37
43
|
listMcpTools,
|
|
38
44
|
loadConfig,
|
|
39
45
|
loadManifestFromDir,
|
|
@@ -64,6 +70,33 @@ function fromMcpName(mcpName: string): string {
|
|
|
64
70
|
return mcpName.replace(/__/g, "/");
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Resolve secrets from the keyring for a tool's environment variables
|
|
75
|
+
* Only resolves variables marked with secret: true in the manifest
|
|
76
|
+
*/
|
|
77
|
+
async function resolveManifestSecrets(
|
|
78
|
+
toolName: string,
|
|
79
|
+
manifest: ToolManifest
|
|
80
|
+
): Promise<Record<string, string>> {
|
|
81
|
+
const envOverrides: Record<string, string> = {};
|
|
82
|
+
|
|
83
|
+
if (!manifest.env) {
|
|
84
|
+
return envOverrides;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [envName, envDecl] of Object.entries(manifest.env)) {
|
|
88
|
+
// Only resolve secrets (not regular env vars)
|
|
89
|
+
if (envDecl && typeof envDecl === "object" && envDecl.secret) {
|
|
90
|
+
const result = await resolveSecret(toolName, envName);
|
|
91
|
+
if (result.found && result.value) {
|
|
92
|
+
envOverrides[envName] = result.value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return envOverrides;
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
/**
|
|
68
101
|
* Convert Enact JSON Schema to MCP tool input schema
|
|
69
102
|
*/
|
|
@@ -111,23 +144,31 @@ async function extractBundle(bundleData: ArrayBuffer, destPath: string): Promise
|
|
|
111
144
|
|
|
112
145
|
mkdirSync(destPath, { recursive: true });
|
|
113
146
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
147
|
+
await new Promise<void>((resolve, reject) => {
|
|
148
|
+
const proc = spawn("tar", ["-xzf", tempFile, "-C", destPath], {
|
|
149
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
150
|
+
});
|
|
118
151
|
|
|
119
|
-
|
|
152
|
+
let stderr = "";
|
|
153
|
+
proc.stderr?.on("data", (data) => {
|
|
154
|
+
stderr += data.toString();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
proc.on("error", reject);
|
|
158
|
+
proc.on("close", (code) => {
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
reject(new Error(`Failed to extract bundle: ${stderr}`));
|
|
161
|
+
} else {
|
|
162
|
+
resolve();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
120
166
|
|
|
121
167
|
try {
|
|
122
168
|
unlinkSync(tempFile);
|
|
123
169
|
} catch {
|
|
124
170
|
// Ignore cleanup errors
|
|
125
171
|
}
|
|
126
|
-
|
|
127
|
-
if (exitCode !== 0) {
|
|
128
|
-
const stderr = await new Response(proc.stderr).text();
|
|
129
|
-
throw new Error(`Failed to extract bundle: ${stderr}`);
|
|
130
|
-
}
|
|
131
172
|
}
|
|
132
173
|
|
|
133
174
|
/**
|
|
@@ -327,6 +368,10 @@ async function handleMetaTool(
|
|
|
327
368
|
const toolInfo = getMcpToolInfo(toolNameArg);
|
|
328
369
|
let manifest: ToolManifest;
|
|
329
370
|
let cachePath: string;
|
|
371
|
+
let bundleHash: string | undefined;
|
|
372
|
+
let toolVersion: string | undefined;
|
|
373
|
+
|
|
374
|
+
const apiClient = getApiClient();
|
|
330
375
|
|
|
331
376
|
if (toolInfo) {
|
|
332
377
|
// Tool is installed, use cached version
|
|
@@ -339,10 +384,19 @@ async function handleMetaTool(
|
|
|
339
384
|
}
|
|
340
385
|
manifest = loaded.manifest;
|
|
341
386
|
cachePath = toolInfo.cachePath;
|
|
387
|
+
toolVersion = toolInfo.version;
|
|
388
|
+
|
|
389
|
+
// Get bundle hash for installed tool from registry
|
|
390
|
+
try {
|
|
391
|
+
const versionInfo = await getToolVersion(apiClient, toolNameArg, toolVersion);
|
|
392
|
+
bundleHash = versionInfo.bundle.hash;
|
|
393
|
+
} catch {
|
|
394
|
+
// Continue without hash - will skip verification
|
|
395
|
+
}
|
|
342
396
|
} else {
|
|
343
397
|
// Tool not installed - fetch and install temporarily
|
|
344
|
-
const apiClient = getApiClient();
|
|
345
398
|
const info = await getToolInfo(apiClient, toolNameArg);
|
|
399
|
+
toolVersion = info.latestVersion;
|
|
346
400
|
|
|
347
401
|
// Download bundle
|
|
348
402
|
const bundleResult = await downloadBundle(apiClient, {
|
|
@@ -350,6 +404,7 @@ async function handleMetaTool(
|
|
|
350
404
|
version: info.latestVersion,
|
|
351
405
|
verify: true,
|
|
352
406
|
});
|
|
407
|
+
bundleHash = bundleResult.hash;
|
|
353
408
|
|
|
354
409
|
// Extract to cache
|
|
355
410
|
cachePath = getToolCachePath(toolNameArg, info.latestVersion);
|
|
@@ -369,6 +424,79 @@ async function handleMetaTool(
|
|
|
369
424
|
manifest = loaded.manifest;
|
|
370
425
|
}
|
|
371
426
|
|
|
427
|
+
// Verify attestations before execution
|
|
428
|
+
let verificationStatus = "⚠️ UNVERIFIED";
|
|
429
|
+
|
|
430
|
+
if (bundleHash && toolVersion) {
|
|
431
|
+
try {
|
|
432
|
+
const verified = await verifyAllAttestations(
|
|
433
|
+
apiClient,
|
|
434
|
+
toolNameArg,
|
|
435
|
+
toolVersion,
|
|
436
|
+
bundleHash
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
if (verified.length > 0) {
|
|
440
|
+
const auditorList = verified.map((v) => v.providerIdentity).join(", ");
|
|
441
|
+
verificationStatus = `✅ VERIFIED by: ${auditorList}`;
|
|
442
|
+
} else {
|
|
443
|
+
verificationStatus = "⚠️ UNVERIFIED - No valid attestations found";
|
|
444
|
+
}
|
|
445
|
+
} catch (verifyErr) {
|
|
446
|
+
verificationStatus = `⚠️ UNVERIFIED - Verification failed: ${verifyErr instanceof Error ? verifyErr.message : "Unknown error"}`;
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
verificationStatus = "⚠️ UNVERIFIED - Could not determine bundle hash";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Enforce trust policy
|
|
453
|
+
const trustPolicy = getTrustPolicy();
|
|
454
|
+
const minimumAttestations = getMinimumAttestations();
|
|
455
|
+
|
|
456
|
+
// Count verified attestations from trusted auditors
|
|
457
|
+
let verifiedCount = 0;
|
|
458
|
+
if (bundleHash && toolVersion) {
|
|
459
|
+
try {
|
|
460
|
+
const verified = await verifyAllAttestations(
|
|
461
|
+
apiClient,
|
|
462
|
+
toolNameArg,
|
|
463
|
+
toolVersion,
|
|
464
|
+
bundleHash
|
|
465
|
+
);
|
|
466
|
+
verifiedCount = verified.filter((v) => isIdentityTrusted(v.providerIdentity)).length;
|
|
467
|
+
} catch {
|
|
468
|
+
// Already handled above in verificationStatus
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Check if trust requirements are met
|
|
473
|
+
if (verifiedCount < minimumAttestations) {
|
|
474
|
+
if (trustPolicy === "require_attestation") {
|
|
475
|
+
return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: `Trust policy violation: Tool requires ${minimumAttestations} attestation(s) from trusted auditors, but only ${verifiedCount} found.\n\nConfigured trust policy: ${trustPolicy}\nTo run unverified tools, update your ~/.enact/config.yaml trust policy to 'allow' or 'prompt'.`,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (trustPolicy === "prompt") {
|
|
486
|
+
// In MCP context, we can't prompt interactively, so we block with a clear message
|
|
487
|
+
return {
|
|
488
|
+
content: [
|
|
489
|
+
{
|
|
490
|
+
type: "text",
|
|
491
|
+
text: `Trust policy violation: Tool requires ${minimumAttestations} attestation(s) from trusted auditors, but only ${verifiedCount} found.\n\nConfigured trust policy: ${trustPolicy}\nMCP server cannot prompt interactively. To run unverified tools via MCP, update your ~/.enact/config.yaml trust policy to 'allow'.`,
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
isError: true,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
// policy === 'allow' - continue execution with warning
|
|
498
|
+
}
|
|
499
|
+
|
|
372
500
|
// Validate and apply defaults
|
|
373
501
|
const inputsWithDefaults = manifest.inputSchema
|
|
374
502
|
? applyDefaults(toolArgs, manifest.inputSchema)
|
|
@@ -385,22 +513,26 @@ async function handleMetaTool(
|
|
|
385
513
|
|
|
386
514
|
const finalInputs = validation.coercedValues ?? inputsWithDefaults;
|
|
387
515
|
|
|
516
|
+
// Resolve secrets from keyring
|
|
517
|
+
const secretOverrides = await resolveManifestSecrets(toolNameArg, manifest);
|
|
518
|
+
|
|
388
519
|
// Execute the tool
|
|
389
520
|
const provider = new DaggerExecutionProvider({ verbose: false });
|
|
390
521
|
await provider.initialize();
|
|
391
522
|
|
|
392
523
|
const result = await provider.execute(
|
|
393
524
|
manifest,
|
|
394
|
-
{ params: finalInputs, envOverrides:
|
|
525
|
+
{ params: finalInputs, envOverrides: secretOverrides },
|
|
395
526
|
{ mountDirs: { [cachePath]: "/workspace" } }
|
|
396
527
|
);
|
|
397
528
|
|
|
398
529
|
if (result.success) {
|
|
530
|
+
const output = result.output?.stdout || "Tool executed successfully (no output)";
|
|
399
531
|
return {
|
|
400
532
|
content: [
|
|
401
533
|
{
|
|
402
534
|
type: "text",
|
|
403
|
-
text:
|
|
535
|
+
text: `[${verificationStatus}]\n\n${output}`,
|
|
404
536
|
},
|
|
405
537
|
],
|
|
406
538
|
};
|
|
@@ -409,7 +541,7 @@ async function handleMetaTool(
|
|
|
409
541
|
content: [
|
|
410
542
|
{
|
|
411
543
|
type: "text",
|
|
412
|
-
text: `
|
|
544
|
+
text: `[${verificationStatus}]\n\nTool execution failed: ${result.error?.message || "Unknown error"}\n\n${result.output?.stderr || ""}`,
|
|
413
545
|
},
|
|
414
546
|
],
|
|
415
547
|
isError: true,
|
|
@@ -604,6 +736,9 @@ function createMcpServer(): Server {
|
|
|
604
736
|
|
|
605
737
|
const finalInputs = validation.coercedValues ?? inputsWithDefaults;
|
|
606
738
|
|
|
739
|
+
// Resolve secrets from keyring
|
|
740
|
+
const secretOverrides = await resolveManifestSecrets(enactToolName, manifest);
|
|
741
|
+
|
|
607
742
|
// Execute the tool using Dagger
|
|
608
743
|
const provider = new DaggerExecutionProvider({
|
|
609
744
|
verbose: false,
|
|
@@ -616,7 +751,7 @@ function createMcpServer(): Server {
|
|
|
616
751
|
manifest,
|
|
617
752
|
{
|
|
618
753
|
params: finalInputs,
|
|
619
|
-
envOverrides:
|
|
754
|
+
envOverrides: secretOverrides,
|
|
620
755
|
},
|
|
621
756
|
{
|
|
622
757
|
mountDirs: {
|