@vibesdotdev/mcp 1.0.0-rc.1 → 1.0.0-rc.5

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.
Files changed (167) hide show
  1. package/README.md +11 -11
  2. package/dist/adapters/index.d.ts +0 -2
  3. package/dist/adapters/index.d.ts.map +1 -1
  4. package/dist/adapters/index.js +0 -2
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/adapters/mcp/connection-identity.d.ts +18 -0
  7. package/dist/adapters/mcp/connection-identity.d.ts.map +1 -1
  8. package/dist/adapters/mcp/connection-identity.js +26 -0
  9. package/dist/adapters/mcp/connection-identity.js.map +1 -1
  10. package/dist/adapters/mcp/dual-era-handler.d.ts +22 -0
  11. package/dist/adapters/mcp/dual-era-handler.d.ts.map +1 -0
  12. package/dist/adapters/mcp/dual-era-handler.js +150 -0
  13. package/dist/adapters/mcp/dual-era-handler.js.map +1 -0
  14. package/dist/adapters/mcp/index.d.ts +0 -4
  15. package/dist/adapters/mcp/index.d.ts.map +1 -1
  16. package/dist/adapters/mcp/index.js +2 -10
  17. package/dist/adapters/mcp/index.js.map +1 -1
  18. package/dist/adapters/mcp/mcp-http-session.d.ts +1 -1
  19. package/dist/adapters/mcp/mcp-http-session.d.ts.map +1 -1
  20. package/dist/adapters/mcp/mcp-http-session.js +1 -1
  21. package/dist/adapters/mcp/mcp-http-session.js.map +1 -1
  22. package/dist/adapters/mcp/projection-doctor.d.ts.map +1 -1
  23. package/dist/adapters/mcp/projection-doctor.js +11 -4
  24. package/dist/adapters/mcp/projection-doctor.js.map +1 -1
  25. package/dist/adapters/mcp/projection.d.ts +1 -1
  26. package/dist/adapters/mcp/projection.d.ts.map +1 -1
  27. package/dist/adapters/mcp/projection.js.map +1 -1
  28. package/dist/adapters/mcp/sampling-context.d.ts.map +1 -1
  29. package/dist/adapters/mcp/sampling-context.js +1 -1
  30. package/dist/adapters/mcp/sampling-context.js.map +1 -1
  31. package/dist/adapters/mcp/sampling.d.ts +1 -1
  32. package/dist/adapters/mcp/sampling.d.ts.map +1 -1
  33. package/dist/adapters/mcp/server.consumer.d.ts +1 -1
  34. package/dist/adapters/mcp/server.consumer.d.ts.map +1 -1
  35. package/dist/adapters/mcp/server.consumer.js +59 -32
  36. package/dist/adapters/mcp/server.consumer.js.map +1 -1
  37. package/dist/adapters/mcp/server.d.ts +1 -22
  38. package/dist/adapters/mcp/server.d.ts.map +1 -1
  39. package/dist/adapters/mcp/server.js +232 -266
  40. package/dist/adapters/mcp/server.js.map +1 -1
  41. package/dist/adapters/mcp/startup-refusal.d.ts +2 -2
  42. package/dist/adapters/mcp/startup-refusal.d.ts.map +1 -1
  43. package/dist/adapters/mcp/startup-refusal.js +10 -10
  44. package/dist/adapters/mcp/startup-refusal.js.map +1 -1
  45. package/dist/adapters/mcp/stdio-relay.d.ts +1 -34
  46. package/dist/adapters/mcp/stdio-relay.d.ts.map +1 -1
  47. package/dist/adapters/mcp/stdio-relay.js +55 -71
  48. package/dist/adapters/mcp/stdio-relay.js.map +1 -1
  49. package/dist/adapters/mcp/tool-approval.d.ts +35 -0
  50. package/dist/adapters/mcp/tool-approval.d.ts.map +1 -0
  51. package/dist/adapters/mcp/tool-approval.js +156 -0
  52. package/dist/adapters/mcp/tool-approval.js.map +1 -0
  53. package/dist/adapters/mcp/tool-audit-live.d.ts +8 -12
  54. package/dist/adapters/mcp/tool-audit-live.d.ts.map +1 -1
  55. package/dist/adapters/mcp/tool-audit-live.js +9 -13
  56. package/dist/adapters/mcp/tool-audit-live.js.map +1 -1
  57. package/dist/adapters/mcp/tool-audit.d.ts +5 -0
  58. package/dist/adapters/mcp/tool-audit.d.ts.map +1 -1
  59. package/dist/adapters/mcp/tool-audit.js +73 -5
  60. package/dist/adapters/mcp/tool-audit.js.map +1 -1
  61. package/dist/adapters/mcp/trust-tag.d.ts.map +1 -1
  62. package/dist/adapters/mcp/trust-tag.js +5 -0
  63. package/dist/adapters/mcp/trust-tag.js.map +1 -1
  64. package/dist/adapters/mcp/web-handler.d.ts +6 -1
  65. package/dist/adapters/mcp/web-handler.d.ts.map +1 -1
  66. package/dist/adapters/mcp/web-handler.js +33 -13
  67. package/dist/adapters/mcp/web-handler.js.map +1 -1
  68. package/dist/adapters/transport-bun-http.d.ts +7 -3
  69. package/dist/adapters/transport-bun-http.d.ts.map +1 -1
  70. package/dist/adapters/transport-bun-http.js +5 -5
  71. package/dist/adapters/transport-bun-http.js.map +1 -1
  72. package/dist/adapters/types.d.ts +7 -9
  73. package/dist/adapters/types.d.ts.map +1 -1
  74. package/dist/adapters/zod-mcp.d.ts.map +1 -1
  75. package/dist/adapters/zod-mcp.js +4 -6
  76. package/dist/adapters/zod-mcp.js.map +1 -1
  77. package/dist/cli/mcp/audit/dev.audit.tools.impl.consumer.d.ts.map +1 -1
  78. package/dist/cli/mcp/audit/dev.audit.tools.impl.consumer.js +6 -1
  79. package/dist/cli/mcp/audit/dev.audit.tools.impl.consumer.js.map +1 -1
  80. package/dist/cli/surface-mcp/dev.mcp.serve.impl.consumer.d.ts.map +1 -1
  81. package/dist/cli/surface-mcp/dev.mcp.serve.impl.consumer.js +2 -0
  82. package/dist/cli/surface-mcp/dev.mcp.serve.impl.consumer.js.map +1 -1
  83. package/dist/config/mcp.config.descriptor.d.ts +28 -33
  84. package/dist/config/mcp.config.descriptor.d.ts.map +1 -1
  85. package/dist/config/mcp.config.descriptor.js +23 -19
  86. package/dist/config/mcp.config.descriptor.js.map +1 -1
  87. package/dist/docs/resources.docs.descriptor.d.ts.map +1 -1
  88. package/dist/docs/resources.docs.descriptor.js +43 -332
  89. package/dist/docs/resources.docs.descriptor.js.map +1 -1
  90. package/dist/docs/server.docs.descriptor.d.ts.map +1 -1
  91. package/dist/docs/server.docs.descriptor.js +71 -300
  92. package/dist/docs/server.docs.descriptor.js.map +1 -1
  93. package/dist/docs/tools.docs.descriptor.d.ts.map +1 -1
  94. package/dist/docs/tools.docs.descriptor.js +69 -244
  95. package/dist/docs/tools.docs.descriptor.js.map +1 -1
  96. package/dist/index.d.ts +7 -7
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +6 -6
  99. package/dist/index.js.map +1 -1
  100. package/dist/mcp.runtime.plugin.d.ts.map +1 -1
  101. package/dist/mcp.runtime.plugin.js +3 -1
  102. package/dist/mcp.runtime.plugin.js.map +1 -1
  103. package/dist/svelte/tool-widgets/summary.d.ts +2 -2
  104. package/dist/svelte/tool-widgets/summary.d.ts.map +1 -1
  105. package/dist/svelte/tool-widgets/summary.js +9 -4
  106. package/dist/tools/vibes-context/schemas.d.ts +2 -1
  107. package/dist/tools/vibes-context/schemas.d.ts.map +1 -1
  108. package/dist/tools/vibes-context/schemas.js +22 -4
  109. package/dist/tools/vibes-context/schemas.js.map +1 -1
  110. package/dist/tools/vibes-context/vibes-context.descriptor.d.ts.map +1 -1
  111. package/dist/tools/vibes-context/vibes-context.descriptor.js +11 -1
  112. package/dist/tools/vibes-context/vibes-context.descriptor.js.map +1 -1
  113. package/dist/tools/vibes-context/vibes-context.impl.d.ts.map +1 -1
  114. package/dist/tools/vibes-context/vibes-context.impl.js +22 -11
  115. package/dist/tools/vibes-context/vibes-context.impl.js.map +1 -1
  116. package/dist/types.d.ts +0 -32
  117. package/dist/types.d.ts.map +1 -1
  118. package/dist/types.js +1 -0
  119. package/dist/types.js.map +1 -1
  120. package/dist/types.registry.d.ts +0 -59
  121. package/dist/types.registry.d.ts.map +1 -1
  122. package/dist/types.registry.js.map +1 -1
  123. package/package.json +62 -93
  124. package/dist/adapters/compatibility.d.ts +0 -20
  125. package/dist/adapters/compatibility.d.ts.map +0 -1
  126. package/dist/adapters/compatibility.js +0 -57
  127. package/dist/adapters/compatibility.js.map +0 -1
  128. package/dist/adapters/host-context.consumer.d.ts +0 -12
  129. package/dist/adapters/host-context.consumer.d.ts.map +0 -1
  130. package/dist/adapters/host-context.consumer.js +0 -10
  131. package/dist/adapters/host-context.consumer.js.map +0 -1
  132. package/dist/adapters/host-context.d.ts +0 -32
  133. package/dist/adapters/host-context.d.ts.map +0 -1
  134. package/dist/adapters/host-context.js +0 -17
  135. package/dist/adapters/host-context.js.map +0 -1
  136. package/dist/adapters/mcp/context.d.ts +0 -137
  137. package/dist/adapters/mcp/context.d.ts.map +0 -1
  138. package/dist/adapters/mcp/context.js +0 -230
  139. package/dist/adapters/mcp/context.js.map +0 -1
  140. package/dist/adapters/mcp/formatters.d.ts +0 -41
  141. package/dist/adapters/mcp/formatters.d.ts.map +0 -1
  142. package/dist/adapters/mcp/formatters.js +0 -102
  143. package/dist/adapters/mcp/formatters.js.map +0 -1
  144. package/dist/adapters/mcp/from-api-endpoint.d.ts +0 -59
  145. package/dist/adapters/mcp/from-api-endpoint.d.ts.map +0 -1
  146. package/dist/adapters/mcp/from-api-endpoint.js +0 -206
  147. package/dist/adapters/mcp/from-api-endpoint.js.map +0 -1
  148. package/dist/adapters/mcp/vibes-types.d.ts +0 -115
  149. package/dist/adapters/mcp/vibes-types.d.ts.map +0 -1
  150. package/dist/adapters/mcp/vibes-types.js +0 -7
  151. package/dist/adapters/mcp/vibes-types.js.map +0 -1
  152. package/dist/mcp/index.d.ts +0 -20
  153. package/dist/mcp/index.d.ts.map +0 -1
  154. package/dist/mcp/index.js +0 -19
  155. package/dist/mcp/index.js.map +0 -1
  156. package/dist/mcp.consumer.plugin.d.ts +0 -4
  157. package/dist/mcp.consumer.plugin.d.ts.map +0 -1
  158. package/dist/mcp.consumer.plugin.js +0 -19
  159. package/dist/mcp.consumer.plugin.js.map +0 -1
  160. package/dist/schemas/host-adapters.d.ts +0 -14
  161. package/dist/schemas/host-adapters.d.ts.map +0 -1
  162. package/dist/schemas/host-adapters.js +0 -7
  163. package/dist/schemas/host-adapters.js.map +0 -1
  164. package/dist/schemas/tool.d.ts +0 -8
  165. package/dist/schemas/tool.d.ts.map +0 -1
  166. package/dist/schemas/tool.js +0 -8
  167. package/dist/schemas/tool.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { CallToolRequestSchema, CallToolResultSchema, TextContentSchema, ResourceLinkSchema, CreateMessageResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
1
+ import mcpPackage from '../../../package.json' with { type: 'json' };
2
+ import { CallToolResultSchema, TextContentSchema, ResourceLinkSchema } from "@modelcontextprotocol/core";
3
+ import { McpServer, ResourceTemplate, ProtocolError, ProtocolErrorCode, acceptedContent, inputRequired, isInputRequiredResult } from "@modelcontextprotocol/server";
3
4
  import z from 'zod/v4';
4
- import { safeParseAsync } from '@modelcontextprotocol/sdk/server/zod-compat.js';
5
+ import { approvalRequestStateCodec } from "./tool-approval.js";
5
6
  const registeredTools = new WeakMap();
6
7
  const explicitResumeInputSchema = z.object({
7
8
  action: z.literal('resume'),
@@ -14,7 +15,7 @@ const receiptRestoreInputSchema = z.object({
14
15
  participantId: z.string()
15
16
  });
16
17
  const explicitSessionTargetSchema = z.object({ sessionId: z.string().trim().min(1) });
17
- import { admitConnectionCaller } from "./connection-identity.js";
18
+ import { admitConnectionCaller, RELAY_ATTESTATION_SOURCE, STDIO_LAUNCH_SOURCE, verifiedConnectionPrincipal } from "./connection-identity.js";
18
19
  import { PROMPTS_MANIFEST_DESCRIPTOR_PREFIX, PROMPTS_MANIFEST_KIND_ID, promptsManifestDescriptorSchema } from '@vibesdotdev/ai-schemas';
19
20
  import { resolvePromptText } from '@vibesdotdev/tools/prompts/manifests';
20
21
  import { isDiscriminatedUnion, getDiscriminator, getDiscriminatorOptions, flattenDiscriminatedUnionToObject, isPlainUnion, resolveMcpDescription } from "../zod-mcp.js";
@@ -28,59 +29,16 @@ import { getLogger } from '@vibesdotdev/logging';
28
29
  import { invokeTool } from '@vibesdotdev/tools/execution/invoke';
29
30
  import { ToolInputValidationError } from '@vibesdotdev/tools/execution/input-validation';
30
31
  import { buildDirectToolCallContext } from '@vibesdotdev/tools/execution/direct-context';
31
- import { DEFAULT_AGENT_CAPABILITY_CEILING, TOOL_AGENT_CONTEXT_METADATA_KEY } from '@vibesdotdev/tools/schemas/runtime/capability-tier';
32
32
  import { isLocalToolId, localToolsOf } from '@vibesdotdev/tools/local/node/ensure-local-tools';
33
- import { detectMcpClientId } from '@vibesdotdev/tools/schemas/presets/registry';
34
- import { loadMcpConfig } from "../../config/load.js";
35
33
  import { formatLocalMcpToolDescription, vibesToolOrigin } from "./local-origin.js";
36
- import { normalizeHarness } from "./serve-identity.js";
37
34
  import { missingRequiredTools, selectMcpTools } from "./tool-selection.js";
38
- import { deriveTrustTier, wrapToolOutputText } from "./trust-tag.js";
35
+ import { deriveTrustTier, shouldDropFromModelContext, wrapToolOutputText } from "./trust-tag.js";
39
36
  import { createProjectionBirthResource, PROJECTION_BIRTH_RESOURCE_URI, projectionCatalogHash } from "./projection.js";
40
37
  import { createMcpWireNameMap, mcpWireToolName } from "./mcp-wire-name.js";
41
38
  import { ambientMeta, formatAmbientPrefix, liftAmbientEnvelope } from "./ambient-envelope.js";
42
39
  import { computeMcpToolCatalog, createMcpToolCatalogCache, formatRegistrationFailureInstructions } from "./tool-catalog.js";
43
40
  import { runWithMcpSamplingCallback } from "./sampling-context.js";
44
- /**
45
- * Resolve the effective capability tier ceiling for an MCP connection from the
46
- * runtime's `mcp` ConfigManifest (`clientCapabilityCeilings`).
47
- *
48
- * Verified transport harness identity (`admitConnectionCaller`) takes
49
- * precedence over server-level `mcpClientId` and `preset`. Unconfigured
50
- * profiles fall back to the tools default ceiling (`'1'`).
51
- */
52
- export function resolveMcpClientCapabilityCeiling(config, input, source) {
53
- const ceilings = config.clientCapabilityCeilings;
54
- const candidates = [];
55
- const pushCandidate = (raw) => {
56
- const trimmed = raw?.trim().toLowerCase();
57
- if (!trimmed)
58
- return;
59
- if (!candidates.includes(trimmed))
60
- candidates.push(trimmed);
61
- const normalized = (source ? normalizeHarness(source, trimmed) : undefined) ?? detectMcpClientId(trimmed) ?? undefined;
62
- if (normalized && !candidates.includes(normalized))
63
- candidates.push(normalized);
64
- };
65
- const verifiedHarness = input.admittedCaller?.harness;
66
- if (verifiedHarness && verifiedHarness !== 'custom') {
67
- pushCandidate(verifiedHarness);
68
- }
69
- pushCandidate(input.mcpClientId);
70
- if (input.preset) {
71
- pushCandidate(input.preset.replace(/^mcp:/i, ''));
72
- }
73
- if (verifiedHarness === 'custom') {
74
- pushCandidate(verifiedHarness);
75
- }
76
- for (const candidate of candidates) {
77
- const configured = ceilings[candidate];
78
- if (configured !== undefined) {
79
- return { profile: candidate, ceiling: configured };
80
- }
81
- }
82
- return { profile: candidates[0], ceiling: DEFAULT_AGENT_CAPABILITY_CEILING };
83
- }
41
+ import { requireMcpToolApproval } from "./tool-approval.js";
84
42
  /**
85
43
  * Project-local catalog ids (CLI `list --source=local` parity). Explicit
86
44
  * options win (tests, serve bootstrap); otherwise the tools this runtime's
@@ -139,7 +97,6 @@ const ToolHookFailureSchema = z.object({
139
97
  metadata: JsonObjectSchema.optional(),
140
98
  alternatives: z.array(z.string()).optional()
141
99
  });
142
- const SafetyReadOnlySchema = z.object({ readOnly: z.boolean().optional() });
143
100
  // SDK metadata and content contracts govern passthrough. Content stays required:
144
101
  // the SDK's default empty array must not classify ordinary domain output as MCP.
145
102
  const mcpToolResponseSchema = CallToolResultSchema.extend({
@@ -223,51 +180,50 @@ function getSuggestedToolCallFromErrorMetadata(metadata) {
223
180
  function isZodLike(value) {
224
181
  return value instanceof z.ZodType;
225
182
  }
226
- function isSafetyCategory(safety) {
227
- return typeof safety === 'string';
228
- }
229
- function deriveMcpAnnotations(safety) {
230
- if (!safety)
231
- return undefined;
232
- const category = isSafetyCategory(safety) ? safety : safety.category;
233
- const hints = isSafetyCategory(safety) ? undefined : safety;
234
- // Default values based on category
235
- const defaults = {
236
- read: {
237
- readOnlyHint: true,
238
- destructiveHint: false,
239
- idempotentHint: true,
240
- openWorldHint: false
241
- },
242
- write: {
243
- readOnlyHint: false,
244
- destructiveHint: true,
245
- idempotentHint: false,
246
- openWorldHint: false
247
- },
248
- execute: {
249
- readOnlyHint: false,
250
- destructiveHint: false,
251
- idempotentHint: false,
252
- openWorldHint: true
253
- }
254
- }[category] ?? {
255
- readOnlyHint: false,
256
- destructiveHint: false,
257
- idempotentHint: false,
258
- openWorldHint: false
183
+ function safetyFacts(value) {
184
+ if (value === undefined)
185
+ return { category: undefined };
186
+ if (typeof value === 'string')
187
+ return { category: value };
188
+ return {
189
+ category: value.category,
190
+ readOnly: value.readOnly,
191
+ idempotent: value.idempotent,
192
+ destructive: value.destructive,
193
+ openWorld: value.openWorld
259
194
  };
260
- // Apply explicit overrides. `readOnly` is read defensively (the
261
- // SafetyField type does not declare it yet) so a descriptor that declares
262
- // an explicit readOnly hint is honored instead of silently taking the
263
- // category default.
264
- const parsedReadOnly = hints ? SafetyReadOnlySchema.safeParse(hints) : undefined;
265
- const readOnly = parsedReadOnly?.success ? parsedReadOnly.data.readOnly : undefined;
195
+ }
196
+ /**
197
+ * MCP annotations are tool-level hints. For a multi-action tool they are the
198
+ * conservative union of the top-level safety and every declared action or
199
+ * variant. A single write/execute action must clear `readOnlyHint`; a single
200
+ * write action must set `destructiveHint`. A descriptor that declares no
201
+ * safety at all is treated as the spec's conservative default (not read-only,
202
+ * destructive, open-world) rather than advertising no hint.
203
+ */
204
+ function deriveMcpAnnotations(source) {
205
+ const top = safetyFacts(source.safety);
206
+ const actions = Object.values(source.actionSafety ?? {}).map((value) => safetyFacts(value));
207
+ const variants = (source.variantSafety ?? []).filter((value) => value !== undefined).map(safetyFacts);
208
+ const all = [top, ...actions, ...variants];
209
+ const categories = all.map((facts) => facts.category).filter((category) => category !== undefined);
210
+ const anyNonRead = categories.some((category) => category !== 'read');
211
+ const anyWrite = categories.includes('write');
212
+ const anyExecute = categories.includes('execute');
213
+ const explicitReadOnly = [top, ...variants].map((facts) => facts.readOnly).find((value) => value !== undefined);
214
+ const explicitDestructive = [top, ...variants].map((facts) => facts.destructive).find((value) => value !== undefined);
215
+ const explicitIdempotent = [top, ...variants].map((facts) => facts.idempotent).find((value) => value !== undefined);
216
+ const explicitOpenWorld = [top, ...variants].map((facts) => facts.openWorld).find((value) => value !== undefined);
217
+ const readOnly = explicitReadOnly ?? (top.category === 'read' && !anyNonRead);
218
+ const destructive = explicitDestructive ?? (anyWrite || variants.some((facts) => facts.destructive === true));
219
+ const idempotent = explicitIdempotent ?? readOnly;
220
+ const declaredNetwork = source.permissions?.some((permission) => permission === 'canAccessNetwork' || permission === 'network') ?? false;
221
+ const openWorld = explicitOpenWorld ?? (anyExecute || declaredNetwork || variants.some((facts) => facts.openWorld === true));
266
222
  return {
267
- readOnlyHint: readOnly ?? defaults.readOnlyHint,
268
- destructiveHint: hints?.destructive ?? defaults.destructiveHint,
269
- idempotentHint: hints?.idempotent ?? defaults.idempotentHint,
270
- openWorldHint: hints?.openWorld ?? defaults.openWorldHint
223
+ readOnlyHint: readOnly,
224
+ destructiveHint: destructive,
225
+ idempotentHint: idempotent,
226
+ openWorldHint: openWorld
271
227
  };
272
228
  }
273
229
  function uniqueName(base, used) {
@@ -306,15 +262,13 @@ function buildResourceText(descriptor, resourceName, uri, variables) {
306
262
  }
307
263
  /** MCP prompt arguments arrive as strings; each manifest-declared argument becomes one. */
308
264
  function buildPromptArgsSchema(argumentsSchema) {
309
- if (!argumentsSchema || argumentsSchema.length === 0)
310
- return undefined;
311
265
  const argumentSchemas = {};
312
- for (const arg of argumentsSchema) {
266
+ for (const arg of argumentsSchema ?? []) {
313
267
  // Describe the outer schema: the SDK lists an optional argument's own description.
314
268
  const description = arg.description ?? `${arg.name} argument`;
315
269
  argumentSchemas[arg.name] = arg.required ? z.string().describe(description) : z.string().optional().describe(description);
316
270
  }
317
- return argumentSchemas;
271
+ return z.object(argumentSchemas);
318
272
  }
319
273
  function registerDescriptorMcpDefinitions(server, descriptors, runtime, logger, options = { announce: true }) {
320
274
  const resourceUris = new Set();
@@ -394,10 +348,18 @@ function registerDescriptorMcpDefinitions(server, descriptors, runtime, logger,
394
348
  .withId(`${PROMPTS_MANIFEST_DESCRIPTOR_PREFIX}${promptId}`)
395
349
  .firstDescriptor();
396
350
  const manifest = registered ? promptsManifestDescriptorSchema.parse(registered).manifest : undefined;
397
- const promptDescription = manifest
398
- ? resolveMcpDescription({ id: promptId, name: manifest.name, description: manifest.description || undefined })
399
- : `Prompt descriptor ${promptId}`;
400
- const argsSchema = buildPromptArgsSchema(manifest?.arguments) ?? {};
351
+ if (!manifest) {
352
+ // A tool names only real prompts/manifest descriptors. A missing
353
+ // descriptor is a registration defect; do not advertise a placeholder.
354
+ mcpLog.error(`Prompt '${promptId}' has no prompts/manifest descriptor; skipping`);
355
+ continue;
356
+ }
357
+ const promptDescription = resolveMcpDescription({
358
+ id: promptId,
359
+ name: manifest.name,
360
+ description: manifest.description || undefined
361
+ });
362
+ const argsSchema = buildPromptArgsSchema(manifest.arguments);
401
363
  server.registerPrompt(name, {
402
364
  description: promptDescription,
403
365
  argsSchema
@@ -411,7 +373,7 @@ function registerDescriptorMcpDefinitions(server, descriptors, runtime, logger,
411
373
  role: 'user',
412
374
  content: {
413
375
  type: 'text',
414
- text: text || `Prompt '${promptId}' has no prompts/manifest content to expose.`
376
+ text: text || `Prompt '${promptId}' rendered no text.`
415
377
  }
416
378
  }
417
379
  ]
@@ -439,25 +401,35 @@ function registerDescriptorMcpDefinitions(server, descriptors, runtime, logger,
439
401
  */
440
402
  export function buildServer(runtime, options, logger, catalog = computeMcpToolCatalog(runtime, options, logger, resolveLocalToolIds(runtime, options))) {
441
403
  const buildLog = logger.child('build');
442
- const serverName = options.serverName ?? 'vibes';
443
- const serverVersion = options.serverVersion ?? '0.0.0';
404
+ const serverName = options.serverName ?? 'mcp';
405
+ const serverVersion = options.serverVersion ?? mcpPackage.version;
444
406
  if (catalog.degradedKind) {
445
407
  // Constitution rule 13: empty tools/list must not look healthy. The
446
408
  // banner was composed (and error-logged) by the catalog fold.
447
409
  return new McpServer({ name: serverName, version: serverVersion }, {
448
410
  capabilities: catalog.capabilities,
449
- instructions: catalog.instructions
411
+ instructions: catalog.instructions,
412
+ requestState: { verify: approvalRequestStateCodec.verify }
450
413
  });
451
414
  }
452
415
  const selected = catalog.selected;
453
416
  if (!selected) {
454
417
  throw new Error('MCP tool catalog is non-degraded but carries no selection');
455
418
  }
419
+ const requiredToolIds = new Set(selected.preset.requiredTools ?? []);
420
+ const missingRequiredLoaders = catalog.zombieCandidates.filter((id) => requiredToolIds.has(id));
421
+ if (missingRequiredLoaders.length > 0) {
422
+ throw new Error(`REQUIRED MCP TOOLS HAVE NO LOADER for preset ${selected.presetId}: ` +
423
+ `${missingRequiredLoaders.join(', ')}. A required continuity tool cannot be advertised ` +
424
+ 'without an implementation; fix the composition before serving.');
425
+ }
456
426
  const enabledDescriptors = catalog.enabledDescriptors;
457
427
  const localToolIds = catalog.localToolIds;
458
428
  const descriptors = catalog.descriptors;
459
429
  createMcpWireNameMap(descriptors.map(descriptor => descriptor.id), options.mcpClientId, catalog.purposePresets);
460
- const capabilities = catalog.capabilities;
430
+ const capabilities = options.buildProvenance && !catalog.capabilities.resources
431
+ ? { ...catalog.capabilities, resources: { listChanged: false } }
432
+ : catalog.capabilities;
461
433
  const instructions = catalog.instructions;
462
434
  const preScanFailedTools = [...catalog.preScanFailedTools];
463
435
  const failedTools = [...preScanFailedTools];
@@ -465,7 +437,8 @@ export function buildServer(runtime, options, logger, catalog = computeMcpToolCa
465
437
  const registeredToolIds = [];
466
438
  const server = new McpServer({ name: serverName, version: serverVersion }, {
467
439
  capabilities,
468
- instructions: instructions || undefined
440
+ instructions: instructions || undefined,
441
+ requestState: { verify: approvalRequestStateCodec.verify }
469
442
  });
470
443
  for (const descriptor of descriptors) {
471
444
  if (!descriptor.schemas?.input) {
@@ -545,100 +518,6 @@ export function buildServer(runtime, options, logger, catalog = computeMcpToolCa
545
518
  appendMcpInstructions(server, birth.instructions);
546
519
  }
547
520
  registerDescriptorMcpDefinitions(server, descriptors, runtime, logger, { announce });
548
- // Client auto-detection (2025-era scar tissue): when serve was spawned without
549
- // --client, the base `mcp` preset applies and client-curated tools stay hidden.
550
- // After initialize, we additive-register the detected preset and emit
551
- // tools/listChanged.
552
- //
553
- // MCP 2026-07-28: tools/list MUST NOT vary per connection. This path mutates
554
- // the catalog after initialize and is therefore gated behind an explicit
555
- // opt-in (`allowClientAutoDetectUpgrade`, default OFF). Pass --client (or
556
- // pack config) so the catalog is fixed at start.
557
- // See docs/proposals/mcp-2026-07-28/FINDINGS.md.
558
- if (!options.mcpClientId && options.allowClientAutoDetectUpgrade) {
559
- const baselineIds = new Set(descriptors.map((descriptor) => descriptor.id));
560
- server.server.oninitialized = () => {
561
- const detected = settleNow(() => {
562
- const clientName = server.server.getClientVersion()?.name;
563
- const detectedClientId = detectMcpClientId(clientName, catalog.purposePresets);
564
- if (!detectedClientId)
565
- return;
566
- buildLog.warn('MCP catalog mutated after initialize (client auto-detect). ' +
567
- 'Illegal under protocol 2026-07-28 — pass --client so the preset is fixed at start.', { clientName, detectedClientId });
568
- // Same registration bound as the construct-time fold: a settled
569
- // surface must not re-admit an unavailable descriptor through
570
- // this opt-in mutation path. `zombieCandidates` is exactly the
571
- // fold's unavailable set.
572
- const settlementExcluded = catalog.loaderSettlement === 'settled' ? new Set(catalog.zombieCandidates) : null;
573
- const upgraded = selectMcpTools(settlementExcluded
574
- ? enabledDescriptors.filter((descriptor) => !settlementExcluded.has(descriptor.id))
575
- : [...enabledDescriptors], { mcpClientId: detectedClientId, customPresets: catalog.purposePresets });
576
- const additions = upgraded.descriptors.filter((descriptor) => !baselineIds.has(descriptor.id));
577
- if (additions.length === 0)
578
- return;
579
- let added = 0;
580
- const upgradeFailures = [];
581
- for (const descriptor of additions) {
582
- const registered = settleNow(() => registerTool(server, descriptor, runtime, { ...options, customPresets: catalog.purposePresets }, logger, localToolIds));
583
- if (!registered.ok) {
584
- const facts = valueFreeFailure(registered.cause);
585
- upgradeFailures.push(`${descriptor.id}: ${facts.name}`);
586
- // Error (not warn): continuity tools often land only via
587
- // this path when --client was omitted at serve start.
588
- buildLog.error(`Client-detected tool failed to register: ${descriptor.id}`, { error: facts });
589
- continue;
590
- }
591
- if (registered.value) {
592
- baselineIds.add(descriptor.id);
593
- added += 1;
594
- }
595
- else {
596
- upgradeFailures.push(`${descriptor.id}: schema conversion failed`);
597
- buildLog.error(`Client-detected tool failed to register: ${descriptor.id}`, { error: 'schema conversion failed' });
598
- }
599
- }
600
- // Initialize already returned construct-time instructions; patch
601
- // so agents that re-read server state / later sessions see the gap.
602
- // Hosts that only saw initialize may still miss this — error log
603
- // remains load-bearing until listChanged + instructions re-push.
604
- if (upgradeFailures.length > 0) {
605
- appendMcpInstructions(server, formatRegistrationFailureInstructions(upgradeFailures));
606
- buildLog.error(`Client-detected preset upgrade incomplete: ${upgradeFailures.length} tool(s) not advertised`, { upgradeFailures, added, detectedClientId });
607
- }
608
- // Required continuity tools for the detected client must not
609
- // vanish silently (brain/presence class outage via auto-detect).
610
- const stillMissingRequired = missingRequiredTools([...baselineIds], upgraded.preset);
611
- if (stillMissingRequired.length > 0) {
612
- const section = `⚠ DEGRADED SURFACE: required tools missing after client auto-detect ` +
613
- `(${detectedClientId}): ${stillMissingRequired.join(', ')}. ` +
614
- `Report this to the user IMMEDIATELY; do NOT silently work around it.`;
615
- appendMcpInstructions(server, section);
616
- buildLog.error('REQUIRED MCP TOOLS MISSING after client auto-detect', {
617
- detectedClientId,
618
- stillMissingRequired
619
- });
620
- }
621
- buildLog.info('Client-detected preset upgrade', {
622
- clientName,
623
- detectedClientId,
624
- presetId: upgraded.presetId,
625
- added,
626
- addedTools: additions.map((descriptor) => descriptor.id),
627
- upgradeFailures
628
- });
629
- });
630
- if (!detected.ok) {
631
- const facts = valueFreeFailure(detected.cause);
632
- // Whole-path failure (not per-tool): still a rule-13 injury —
633
- // thin base preset can look healthy without continuity tools.
634
- appendMcpInstructions(server, '⚠ DEGRADED SURFACE: client preset auto-detection failed after initialize. ' +
635
- 'Continuity tools for the connected client may be missing. ' +
636
- 'Report this to the user IMMEDIATELY; do NOT silently work around it. ' +
637
- 'Diagnose: pass --client explicitly or check MCP initialize clientInfo.name.');
638
- buildLog.error('Client preset auto-detection failed', { error: facts });
639
- }
640
- };
641
- }
642
521
  // Own admission before asynchronous SDK schema parsing. RegisteredTool is
643
522
  // a public SDK handle; replacing it leaves admitted calls' schemas intact.
644
523
  //
@@ -648,18 +527,26 @@ export function buildServer(runtime, options, logger, catalog = computeMcpToolCa
648
527
  // failures stay tool-result-shaped but stamp `_meta.errorCode`
649
528
  // (`INVALID_INPUT`), mirroring the execution path's capability-error
650
529
  // projection below.
651
- server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
530
+ server.server.setRequestHandler('tools/call', async (request, ctx) => {
652
531
  const invocation = runtime.acquireInvocation();
653
532
  try {
654
533
  const attempted = await settle(() => invocation.runtime.runInvocation(async () => {
655
534
  const tool = registeredTools.get(server)?.get(request.params.name);
656
535
  if (!tool?.registration.enabled)
657
- throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
536
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
658
537
  if (request.params.task)
659
- throw new McpError(ErrorCode.InvalidParams, 'This tool does not support task execution');
660
- const parsed = tool.registration.inputSchema ? await safeParseAsync(tool.registration.inputSchema, request.params.arguments) : { success: true, data: request.params.arguments };
661
- if (!parsed.success) {
662
- const issues = formatZodIssues(parsed.error);
538
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'This tool does not support task execution');
539
+ const schema = tool.registration.inputSchema;
540
+ const validated = schema
541
+ ? await schema['~standard'].validate(request.params.arguments)
542
+ : { value: request.params.arguments };
543
+ if ('issues' in validated && validated.issues) {
544
+ const issues = formatZodIssues({
545
+ issues: validated.issues.map((issue) => ({
546
+ path: issue.path?.map((part) => typeof part === 'object' && part !== null && 'key' in part ? part.key : part),
547
+ message: issue.message
548
+ }))
549
+ });
663
550
  const text = `Invalid input for '${request.params.name}': ${issues}`;
664
551
  // Omit structuredContent on isError: MCP clients that kept
665
552
  // outputSchema validate it even on errors, and {ok:false,error}
@@ -670,13 +557,19 @@ export function buildServer(runtime, options, logger, catalog = computeMcpToolCa
670
557
  _meta: { errorCode: 'INVALID_INPUT' }
671
558
  };
672
559
  }
673
- return await tool.invoke(request.params.arguments, extra, invocation);
560
+ const result = await tool.invoke(request.params.arguments, ctx, invocation);
561
+ // Low-level tools/call handlers own the era projection the SDK's
562
+ // built-in handler would have applied: the 2025 wire codec wraps a
563
+ // non-object output schema and its structuredContent as `{result}`.
564
+ return isInputRequiredResult(result)
565
+ ? result
566
+ : server.server.projectCallToolResult(result, tool.registration.outputSchemaJson);
674
567
  }));
675
568
  if (attempted.ok)
676
569
  return attempted.value;
677
570
  // McpError must escape so the SDK answers with a JSON-RPC error
678
571
  // response carrying its code and the request id.
679
- if (attempted.cause instanceof McpError)
572
+ if (attempted.cause instanceof ProtocolError)
680
573
  throw attempted.cause;
681
574
  const failure = projectMcpToolFailure(attempted.cause);
682
575
  return {
@@ -791,9 +684,30 @@ function normalizableOutputSchema(rawOutputSchema) {
791
684
  const cached = normalizableOutputSchemas.get(rawOutputSchema);
792
685
  if (cached)
793
686
  return cached.schema;
794
- const schema = !isPlainUnion(rawOutputSchema) && !isDiscriminatedUnion(rawOutputSchema)
795
- ? rawOutputSchema
796
- : undefined;
687
+ let schema;
688
+ // MCP's 2025 wire requires an object-root output schema, and every
689
+ // structuredContent this server emits is an object. A union of object
690
+ // variants is object-shaped on every branch, so it is advertised: the
691
+ // 2025 codec wraps its non-object root and its structuredContent together.
692
+ // A non-object root (`z.unknown()`, arrays, primitives, mixed unions)
693
+ // would make clients demand structuredContent the text wire path never
694
+ // produces, so it is not advertised at all.
695
+ const isObjectOutput = (value) => {
696
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
697
+ return false;
698
+ const json = value;
699
+ if (json['type'] === 'object')
700
+ return true;
701
+ for (const key of ['anyOf', 'oneOf']) {
702
+ const branches = json[key];
703
+ if (Array.isArray(branches) && branches.length > 0)
704
+ return branches.every(isObjectOutput);
705
+ }
706
+ return false;
707
+ };
708
+ const json = settleNow(() => rawOutputSchema.toJSONSchema());
709
+ if (json.ok && isObjectOutput(json.value))
710
+ schema = rawOutputSchema;
797
711
  normalizableOutputSchemas.set(rawOutputSchema, { schema });
798
712
  return schema;
799
713
  }
@@ -823,13 +737,16 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
823
737
  // safeParseAsync(undefined, ...) which throws "undefined is not an object (evaluating 's._zod')".
824
738
  // Skip output schemas that are plain unions or discriminated unions to avoid this SDK bug.
825
739
  const rawOutputSchema = descriptor.schemas.output === undefined ? undefined : requireToolZodSchema(descriptor.schemas.output, descriptor.id);
826
- const outputSchema = rawOutputSchema ? normalizableOutputSchema(rawOutputSchema) : undefined;
827
- // Derive MCP annotations from safety field
828
- const annotations = deriveMcpAnnotations(descriptor.safety);
829
740
  // Trust-tag tier is derived from `safety` until `descriptor.capability.tier`
830
741
  // lands in the tier-spec PR (AI-SURFACE-2026-05.md §4.3 step 1). Captured
831
742
  // once at registration so it composes with cache-stable system prompts.
832
743
  const trustTier = deriveTrustTier(descriptor);
744
+ // A tier-3 result deliberately carries no structuredContent. Advertising
745
+ // an output schema would make a modern client demand content that this
746
+ // surface never emits, so the schema is not advertised at all.
747
+ const outputSchema = rawOutputSchema && !shouldDropFromModelContext(trustTier)
748
+ ? normalizableOutputSchema(rawOutputSchema)
749
+ : undefined;
833
750
  const toolAccessor = runtime.assets('tools/tool');
834
751
  const isDiscriminated = toolAccessor.isDiscriminated(descriptor.id);
835
752
  let variantSafety;
@@ -844,12 +761,25 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
844
761
  }
845
762
  }
846
763
  }
764
+ // MCP annotations are the conservative union of the top-level safety and
765
+ // every per-action/variant safety. A write action can never be advertised
766
+ // as read-only just because the tool's default action is a read.
767
+ const annotationSource = {
768
+ safety: descriptor.safety,
769
+ permissions: descriptor.permissions
770
+ };
771
+ if (descriptor.schemas.actionSafety)
772
+ annotationSource.actionSafety = descriptor.schemas.actionSafety;
773
+ if (variantSafety)
774
+ annotationSource.variantSafety = Object.values(variantSafety);
775
+ const annotations = deriveMcpAnnotations(annotationSource);
847
776
  const isLocal = isLocalToolId(descriptor.id, localToolIds);
848
777
  const origin = vibesToolOrigin(isLocal);
849
778
  const description = formatLocalMcpToolDescription(getDescriptorMcpDescription(descriptor), isLocal);
850
779
  const dispatchRuntime = runtime;
851
780
  const vibesRegistrationMeta = {
852
781
  origin,
782
+ toolId: descriptor.id,
853
783
  category: descriptor.category,
854
784
  subcategory: descriptor.subcategory,
855
785
  tags: descriptor.tags,
@@ -857,6 +787,8 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
857
787
  safety: descriptor.safety,
858
788
  mcpClientId: options.mcpClientId
859
789
  };
790
+ if (descriptor.schemas.actionSafety)
791
+ vibesRegistrationMeta.actionSafety = descriptor.schemas.actionSafety;
860
792
  if (variantSafety)
861
793
  vibesRegistrationMeta.variants = variantSafety;
862
794
  const registrationOptions = {
@@ -867,7 +799,7 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
867
799
  annotations,
868
800
  _meta: { vibes: vibesRegistrationMeta }
869
801
  };
870
- const handler = async (args, extra, boundInvocation) => {
802
+ const handler = async (args, ctx, boundInvocation) => {
871
803
  const callId = `mcp_${Date.now()}_${crypto.randomUUID()}`;
872
804
  // Per-call nonce for trust-tag wrapper (F4). Shared across all text items
873
805
  // and error paths in this tool call so the model can verify boundary integrity.
@@ -879,7 +811,7 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
879
811
  const stalePrefix = options.staleBodyBanner?.() ?? null;
880
812
  let toolCallCtx;
881
813
  const callStarted = Date.now();
882
- const traceStage = (stage) => logger.info('MCP invocation stage', { callId, tool: descriptor.id, stage, elapsedMs: Date.now() - callStarted });
814
+ const traceStage = (stage) => logger.debug('MCP invocation stage', { callId, tool: descriptor.id, stage, elapsedMs: Date.now() - callStarted });
883
815
  traceStage('start');
884
816
  const invocation = boundInvocation?.retain() ?? dispatchRuntime.acquireInvocation();
885
817
  const runtime = invocation.runtime;
@@ -910,15 +842,14 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
910
842
  resolvedArgs = { [discriminator]: defaultAction, ...resolvedArgs };
911
843
  }
912
844
  }
913
- // The SDK callback does not currently populate `extra.sessionId` for
914
- // WebStandard Streamable HTTP. The HTTP adapter therefore gives each
915
- // connection a private identity cell when it builds the server. This is
916
- // call data, never mutable process-global runtime scope.
917
- const transportSessionId = extra.sessionId ?? options.resolveTransportSessionId?.();
845
+ // A 2025-era transport session is call data, never mutable
846
+ // process-global runtime scope. A 2026-era request carries its
847
+ // explicit session handle in the tool input instead.
848
+ const transportSessionId = ctx.sessionId ?? options.resolveTransportSessionId?.();
918
849
  // A session-family call may name the participant it continues as
919
850
  // (the documented bind flow). That is a binding hint the session layer
920
851
  // verifies, not identity: agent identity comes only from the connection.
921
- const callMeta = '_meta' in extra ? asJsonObject(extra._meta) : undefined;
852
+ const callMeta = asJsonObject(ctx.mcpReq._meta);
922
853
  const metaParticipantId = (isStringValue(callMeta?.participantId) && callMeta.participantId) ||
923
854
  (isStringValue(callMeta?.['vibes/participantId']) &&
924
855
  callMeta['vibes/participantId']) ||
@@ -929,15 +860,22 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
929
860
  callId,
930
861
  logger: logger.child(callDescriptor.id)
931
862
  };
932
- if (extra.signal)
933
- baseToolCallCtx.signal = extra.signal;
934
- if (extra.sendProgress)
863
+ baseToolCallCtx.signal = ctx.mcpReq.signal;
864
+ const progressToken = ctx.mcpReq._meta?.progressToken;
865
+ if (progressToken !== undefined) {
935
866
  baseToolCallCtx.sendProgress = (progress) => {
936
- const update = { progress: progress.progress, total: progress.total };
867
+ const update = {
868
+ progressToken
869
+ };
870
+ if (progress.progress !== undefined)
871
+ update.progress = progress.progress;
872
+ if (progress.total !== undefined)
873
+ update.total = progress.total;
937
874
  if (progress.message)
938
875
  update.message = progress.message;
939
- extra.sendProgress?.(update);
876
+ void ctx.mcpReq.notify({ method: 'notifications/progress', params: update });
940
877
  };
878
+ }
941
879
  // Stamp trusted identity from the runtime's `auth/identity` (the same
942
880
  // builder the tools-cli surface uses). The MCP stdio surface runs as an
943
881
  // authenticated local account; without this, owner-scoped tools
@@ -951,33 +889,42 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
951
889
  : { ...directContext.caller };
952
890
  // Admission (RC08 U7): identity only from what this connection's
953
891
  // transport verified, before the hook bus or any observer runs.
892
+ const verifiedConnection = options.resolveVerifiedConnectionIdentity?.() ?? options.verifiedConnectionIdentity;
893
+ if ((verifiedConnection?.source === STDIO_LAUNCH_SOURCE ||
894
+ verifiedConnection?.source === RELAY_ATTESTATION_SOURCE) &&
895
+ typeof directContext.metadata?.actorUserId === 'string' &&
896
+ !directContext.metadata.orgId &&
897
+ runtime.hasContext('orgId')) {
898
+ const actorUserId = directContext.metadata.actorUserId;
899
+ const resolvedOrg = await settle(() => runtime.runInvocation(() => runtime.context('orgId'), {
900
+ actor: { userId: actorUserId }
901
+ }));
902
+ if (resolvedOrg.ok && typeof resolvedOrg.value === 'string' && resolvedOrg.value.trim().length > 0) {
903
+ const orgId = resolvedOrg.value.trim();
904
+ directContext.metadata.orgId = orgId;
905
+ directContext.metadata.organizationId = orgId;
906
+ }
907
+ }
954
908
  const admittedCaller = admitConnectionCaller({
955
- verified: options.resolveVerifiedConnectionIdentity?.() ?? options.verifiedConnectionIdentity,
909
+ verified: verifiedConnection,
956
910
  declared: options.resolveConnectionIdentity?.(),
957
911
  call: caller,
958
912
  principal: directContext.metadata
959
913
  });
960
- const mcpSettings = await loadMcpConfig(runtime);
961
- const resolvedCeiling = resolveMcpClientCapabilityCeiling(mcpSettings, {
962
- admittedCaller,
963
- mcpClientId: options.mcpClientId,
964
- preset: options.preset
965
- }, runtime);
966
- const agentContext = {
967
- agentId: admittedCaller?.agentId ?? resolvedCeiling.profile ?? 'mcp-client',
968
- ceiling: resolvedCeiling.ceiling,
969
- callChain: []
970
- };
914
+ // A harness is a transport, not a role. The call carries the
915
+ // direct consumer surface identity; the tools owner's policy is the
916
+ // only place that decides whether a tool is in bounds.
971
917
  const metadata = {
972
918
  ...directContext.metadata,
973
- 'mcp.transportMode': options.mode,
974
- [TOOL_AGENT_CONTEXT_METADATA_KEY]: agentContext,
975
- agentCapabilityCeiling: resolvedCeiling.ceiling
919
+ 'mcp.transportMode': options.mode
976
920
  };
977
921
  if (transportSessionId)
978
922
  metadata['mcp.transportSessionId'] = transportSessionId;
979
923
  if (connectionKey)
980
924
  metadata['mcp.connectionKey'] = connectionKey;
925
+ // The 2026 HTTP request owns a fresh server instance; explicit
926
+ // session handles, not an ambient binding, are its continuity.
927
+ metadata['mcp.protocolEra'] = ctx.mcpReq.envelope !== undefined ? 'modern' : 'legacy';
981
928
  const directToolCallCtx = {
982
929
  ...directContext,
983
930
  caller: admittedCaller,
@@ -994,15 +941,14 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
994
941
  : directToolCallCtx;
995
942
  traceStage('stamp-session');
996
943
  const sessionStampedCtx = await stampMcpSessionCaller(runtime, server, preparedToolCallCtx, transportSessionId ?? connectionKey, options.mode, callDescriptor.id, resolvedArgs, logger);
997
- const stampedToolCallCtx = {
998
- ...sessionStampedCtx,
999
- metadata: {
1000
- ...sessionStampedCtx.metadata,
1001
- [TOOL_AGENT_CONTEXT_METADATA_KEY]: agentContext,
1002
- agentCapabilityCeiling: resolvedCeiling.ceiling
1003
- }
1004
- };
944
+ const stampedToolCallCtx = sessionStampedCtx;
1005
945
  toolCallCtx = stampedToolCallCtx;
946
+ // The tool owner declares the approval contract; this surface carries
947
+ // the question to the connected client and fails closed when it cannot.
948
+ // Written in the multi-round-trip form so 2026 and 2025 share it.
949
+ const approval = await requireMcpToolApproval(ctx, callDescriptor, resolvedArgs);
950
+ if (approval)
951
+ return approval;
1006
952
  // Route through invokeTool so tools/tool hook bus events
1007
953
  // (contextBuild / beforeExecute / afterExecute / error) fire
1008
954
  // around execution. invokeTool handles discriminated dispatch
@@ -1014,21 +960,27 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
1014
960
  if (options.toolExecutionTimeoutMs !== undefined && options.toolExecutionTimeoutMs > 0) {
1015
961
  execution.timeoutMs = options.toolExecutionTimeoutMs;
1016
962
  }
1017
- const detachAgentContextHook = runtime
1018
- .hooks('tools/tool')
1019
- .on('contextBuild', (event) => {
1020
- if (event.callId === stampedToolCallCtx.callId) {
1021
- event.metadata.set(TOOL_AGENT_CONTEXT_METADATA_KEY, agentContext);
1022
- }
1023
- });
1024
- const rawOutput = await (async () => {
1025
- try {
1026
- return await runWithMcpSamplingCallback(options.scopedValues, (request) => server.server.request({ method: 'sampling/createMessage', params: request }, CreateMessageResultSchema, { relatedRequestId: extra.requestId }), () => invokeTool(execution));
963
+ const principal = verifiedConnectionPrincipal({ verified: verifiedConnection, metadata: directContext.metadata });
964
+ const rawOutput = await runWithMcpSamplingCallback(options.scopedValues, (request) => {
965
+ if (options.sampling === 'off') {
966
+ throw new CapabilityError('MCP sampling is disabled by configuration.', 'UNAVAILABLE', { reason: 'sampling-disabled', retryable: false });
1027
967
  }
1028
- finally {
1029
- detachAgentContextHook();
968
+ // 2026-07-28 removed the server→client request channel.
969
+ // Sampling is deprecated in that revision; a tool that
970
+ // needs a model call must use its own provider rather
971
+ // than hang on a channel the revision does not carry.
972
+ if (ctx.mcpReq.envelope !== undefined) {
973
+ throw new CapabilityError('MCP sampling is unavailable on protocol revision 2026-07-28; the tool must call its own model provider.', 'UNAVAILABLE', { reason: 'sampling-unavailable-modern', retryable: false });
1030
974
  }
1031
- })();
975
+ return ctx.mcpReq.requestSampling(request,
976
+ // Keep the server-to-client request on this call's
977
+ // response stream: a relayed stdio body reads that
978
+ // stream, not a standalone SSE channel.
979
+ { relatedRequestId: ctx.mcpReq.id });
980
+ },
981
+ // A verified connection is this host's own account: the call runs as
982
+ // its request principal, so caller-bearer clients see the login.
983
+ () => principal ? runtime.runInvocation(() => invokeTool(execution), { principal }) : invokeTool(execution));
1032
984
  traceStage('result');
1033
985
  const outputSchema = callDescriptor.schemas.output;
1034
986
  const lifted = liftAmbientEnvelope(rawOutput, isZodLike(outputSchema) ? outputSchema : undefined);
@@ -1071,6 +1023,8 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
1071
1023
  ...mcpResponse,
1072
1024
  content: wrappedContent
1073
1025
  };
1026
+ if (shouldDropFromModelContext(trustTier))
1027
+ delete result.structuredContent;
1074
1028
  if (envelopeMeta)
1075
1029
  result._meta = { ...mcpResponse._meta, ...envelopeMeta };
1076
1030
  return result;
@@ -1124,7 +1078,9 @@ function registerTool(server, descriptor, runtime, options, logger, localToolIds
1124
1078
  }
1125
1079
  const result = { content };
1126
1080
  // Plain object outputs retain typed data without requiring clients to parse text.
1127
- if (wired.structured)
1081
+ // A tier-3 result is dropped from model context in BOTH text and
1082
+ // structured form; leaving structuredContent would leak the payload.
1083
+ if (wired.structured && !shouldDropFromModelContext(trustTier))
1128
1084
  result.structuredContent = wired.structured;
1129
1085
  if (envelopeMeta)
1130
1086
  result._meta = envelopeMeta;
@@ -1316,6 +1272,16 @@ async function stampMcpSessionCaller(runtime, server, ctx, transportSessionId, m
1316
1272
  });
1317
1273
  }
1318
1274
  const isSessionFamilyTool = toolId === 'session' || toolId.startsWith('session-');
1275
+ // A session-family call that names its durable target does not need an
1276
+ // ambient transport binding. This is the normal shape on a 2026 HTTP
1277
+ // request (one fresh server per request) and on a 2025 client that has
1278
+ // not run `session init` on this connection yet.
1279
+ const hasExplicitSessionTarget = mode === 'http' &&
1280
+ ctx.metadata?.['mcp.protocolEra'] === 'modern' &&
1281
+ isSessionFamilyTool &&
1282
+ ((typeof input['sessionId'] === 'string' && input['sessionId'].trim().length > 0) ||
1283
+ ['todoId', 'messageId'].some((key) => typeof input[key] === 'string' && input[key].trim().length > 0) ||
1284
+ ['sessionIds', 'todoIds', 'messageIds'].some((key) => Array.isArray(input[key]) && input[key].length > 0));
1319
1285
  const { participantId: _hintParticipant, sessionId: _hintSession, ...unboundCaller } = ctx.caller ?? {};
1320
1286
  if (!runtime.hasContext('session/manager')) {
1321
1287
  return { ...ctx, caller: unboundCaller };
@@ -1377,7 +1343,7 @@ async function stampMcpSessionCaller(runtime, server, ctx, transportSessionId, m
1377
1343
  // participant identity, but without borrowing the closed session's custody.
1378
1344
  const explicitResume = toolId === 'session' && explicitResumeInputSchema.safeParse(input).success;
1379
1345
  if (!getNeedsId &&
1380
- (explicitResume || sessionUtils?.isSessionDiscoveryCall(toolId, input.action) ||
1346
+ (hasExplicitSessionTarget || explicitResume || sessionUtils?.isSessionDiscoveryCall(toolId, input.action) ||
1381
1347
  (!sessionUtils &&
1382
1348
  toolId === 'session' &&
1383
1349
  (input.action === 'init' || input.action === 'status')))) {