@nanobpm/urban 0.84.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -408,3 +408,19 @@ test("/api/complete rejects a body with no userTaskKey", async () => {
408
408
  assert.equal(res.status, 400);
409
409
  });
410
410
 
411
+
412
+ test("MCP is listed as an active surface only when config-enabled, but its route is always mounted", () => {
413
+ // Default: MCP enabled -> listed as an active surface.
414
+ const on = mountSurfaces(ctxWith({}), fakeApp);
415
+ const onEnabled = on.describe().enabled;
416
+ assert.ok(Array.isArray(onEnabled) && onEnabled.includes("mcp@/app/mcp"), "an enabled MCP surface is listed as active");
417
+
418
+ // Config-disabled (URBAN_MCP_ENABLED=false): the handler 404s, so it must NOT appear as active,
419
+ // yet the route stays mounted for a stable address.
420
+ const base = ctxWith({});
421
+ const offCtx = { ...base, host: { ...base.host, env: (v: string) => (v === "URBAN_MCP_ENABLED" ? "false" : undefined) } };
422
+ const off = mountSurfaces(offCtx, fakeApp);
423
+ const offEnabled = off.describe().enabled;
424
+ assert.ok(Array.isArray(offEnabled) && !offEnabled.includes("mcp@/app/mcp"), "a disabled MCP surface must not be listed as active");
425
+ assert.ok(off.routes.some((r) => r.path === "/app/mcp"), "the MCP route stays mounted even when disabled");
426
+ });
@@ -14,6 +14,7 @@ import { completeUserTaskResponse, resolveFormResponse } from "./forms.ts";
14
14
  import { mountActions } from "./actions.ts";
15
15
  import { mountAgent } from "./agent.ts";
16
16
  import { mountApi } from "./api.ts";
17
+ import { mountMcp } from "./mcp.ts";
17
18
  import { mountPages } from "./pages.ts";
18
19
 
19
20
  /** Escape HTML-significant characters before embedding a value in markup. */
@@ -211,6 +212,16 @@ export function mountSurfaces(ctx: RuntimeContext, app: AppApi): SurfacesHandle
211
212
  routes.push(...agent.routes);
212
213
  enabled.push("agent@/app/agent");
213
214
 
215
+ // The runtime-served MCP surface (ADR 0067): `/app/mcp`, a Streamable-HTTP MCP endpoint mounted
216
+ // unconditionally (like `/app/agent`) so an MCP client always has a stable address. It projects
217
+ // the app's read-only OpenAPI operations as tools from the SAME enumeration `mountApi` routes
218
+ // from, plus framework-owned read-only process-debugging tools — zero app-side MCP code.
219
+ const mcp = mountMcp(ctx, app, api.routes);
220
+ routes.push(...mcp.routes);
221
+ // The route is always mounted (stable address), but only claim MCP as an active surface when it
222
+ // actually answers — a config-disabled surface 404s, so listing it here would be inaccurate.
223
+ if (mcp.enabled) enabled.push("mcp@/app/mcp");
224
+
214
225
  // The pages surface (the schema-driven page runtime) mounts its own routes.
215
226
  const pages = mountPages(ctx, app);
216
227
  if (pages.routes.length > 0) {