@nanobpm/urban 0.84.0 → 0.85.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.
@@ -12,6 +12,7 @@ import { completeUserTaskResponse, resolveFormResponse } from "./forms.js";
12
12
  import { mountActions } from "./actions.js";
13
13
  import { mountAgent } from "./agent.js";
14
14
  import { mountApi } from "./api.js";
15
+ import { mountMcp } from "./mcp.js";
15
16
  import { mountPages } from "./pages.js";
16
17
  /** Escape HTML-significant characters before embedding a value in markup. */
17
18
  function escapeHtml(s) {
@@ -190,6 +191,16 @@ export function mountSurfaces(ctx, app) {
190
191
  const agent = mountAgent(ctx);
191
192
  routes.push(...agent.routes);
192
193
  enabled.push("agent@/app/agent");
194
+ // The runtime-served MCP surface (ADR 0067): `/app/mcp`, a Streamable-HTTP MCP endpoint mounted
195
+ // unconditionally (like `/app/agent`) so an MCP client always has a stable address. It projects
196
+ // the app's read-only OpenAPI operations as tools from the SAME enumeration `mountApi` routes
197
+ // from, plus framework-owned read-only process-debugging tools — zero app-side MCP code.
198
+ const mcp = mountMcp(ctx, app, api.routes);
199
+ routes.push(...mcp.routes);
200
+ // The route is always mounted (stable address), but only claim MCP as an active surface when it
201
+ // actually answers — a config-disabled surface 404s, so listing it here would be inaccurate.
202
+ if (mcp.enabled)
203
+ enabled.push("mcp@/app/mcp");
193
204
  // The pages surface (the schema-driven page runtime) mounts its own routes.
194
205
  const pages = mountPages(ctx, app);
195
206
  if (pages.routes.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.84.0",
3
+ "version": "0.85.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -99,6 +99,7 @@
99
99
  "test:deno": "deno test -A --no-check src/"
100
100
  },
101
101
  "dependencies": {
102
+ "@modelcontextprotocol/sdk": "^1.30.0",
102
103
  "@nanobpm/nano-app-schema": "^0.9.0",
103
104
  "@nanobpm/nano-sdk": "^1.2.5",
104
105
  "@nanobpm/workflow": "^0.14.0",
@@ -55,7 +55,7 @@ export interface ApiBinding {
55
55
  /** The canonical, non-configurable mount prefix for an app's OpenAPI operations. Operations always
56
56
  * live under `/app/api` so page actions, links, and any `callRoute` can name an operation path as a
57
57
  * stable framework constant (`/app/api/<op path>`) — there is no per-app `base` knob to drift. */
58
- const API_BASE = "/app/api";
58
+ export const API_BASE = "/app/api";
59
59
 
60
60
  /** Pinned Swagger UI dist served from a CDN, so the docs UI adds zero bundle/runtime deps to an
61
61
  * Urban app (matching the deps-free spirit of the rest of the surface — validators are standalone,
@@ -250,7 +250,7 @@ export function resolveOperationHandler(mod: Record<string, unknown>): Operation
250
250
  return undefined;
251
251
  }
252
252
 
253
- function readApiBinding(manifest: unknown): ApiBinding | undefined {
253
+ export function readApiBinding(manifest: unknown): ApiBinding | undefined {
254
254
  const raw = manifest && typeof manifest === "object" ? Reflect.get(manifest, "api") : undefined;
255
255
  if (!raw || typeof raw !== "object") return undefined;
256
256
  const spec = Reflect.get(raw, "spec");