@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enactprotocol/mcp-server",
3
- "version": "2.2.1",
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.1",
28
- "@enactprotocol/execution": "2.2.1",
29
- "@enactprotocol/shared": "2.2.1",
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
- const proc = Bun.spawn(["tar", "-xzf", tempFile, "-C", destPath], {
115
- stdout: "pipe",
116
- stderr: "pipe",
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
- const exitCode = await proc.exited;
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: result.output?.stdout || "Tool executed successfully (no output)",
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: `Tool execution failed: ${result.error?.message || "Unknown error"}\n\n${result.output?.stderr || ""}`,
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: {