@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.
- package/dist/openapi/spec.d.ts +72 -0
- package/dist/openapi/spec.js +130 -0
- package/dist/runtime/core/modules/api.d.ts +5 -0
- package/dist/runtime/core/modules/api.js +2 -2
- package/dist/runtime/core/modules/mcp.d.ts +75 -0
- package/dist/runtime/core/modules/mcp.js +994 -0
- package/dist/runtime/core/modules/surfaces.js +11 -0
- package/package.json +4 -1
- package/src/openapi/mcp-parity.fixture.yaml +49 -0
- package/src/openapi/mcp-tools.snapshot.json +20 -0
- package/src/openapi/spec.test.ts +92 -0
- package/src/openapi/spec.ts +161 -0
- package/src/runtime/core/modules/api.ts +2 -2
- package/src/runtime/core/modules/mcp.test.ts +1199 -0
- package/src/runtime/core/modules/mcp.ts +1144 -0
- package/src/runtime/core/modules/surfaces.test.ts +16 -0
- package/src/runtime/core/modules/surfaces.ts +11 -0
|
@@ -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) {
|