@happyvertical/smrt-app-mcp 0.53.3 → 0.53.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/AGENTS.md CHANGED
@@ -22,6 +22,18 @@ tool policy; `./sveltekit` owns the stateless Streamable HTTP transport.
22
22
  validates JWT access tokens and requires an application-owned principal mapper.
23
23
  - Mount `mountMcpRoute` as one modern `POST` endpoint. It creates a fresh
24
24
  protocol server per request, emits no session id, and refuses subscriptions.
25
+ - `mountMcpAppRoute` (`src/defaults.ts` + `./sveltekit`) is the one-call app
26
+ route: explicit `models` allow-list, required `requiredScopes`, principal from
27
+ session locals (`principalFromSessionLocals`). App policies compose with the
28
+ default and may only narrow it. Its `Origin` check (default on; opt-in on
29
+ `mountMcpRoute`) runs before auth and dispatch; no-`Origin` clients pass.
30
+ `effects` filters the catalog by `mcpToolEffect` (excluded tools 404).
31
+ `auth` swaps in bearer principals; `bindPrincipal` runs their dispatch in
32
+ the app's principal context (RLS transaction), failing closed, and
33
+ authorizes with the binder's narrowed (live) scopes; task lifecycle calls
34
+ re-check the originating action against the allow-list and `effects`,
35
+ after a principal-level `taskPrincipalPolicy` (defaults: scope policy);
36
+ `mountMcpProtectedResourceMetadataRoute` serves only the advertised path.
25
37
  - Keep tool catalogs private by default. Public caching requires an explicit
26
38
  attestation and a global, unauthenticated, read-only, non-tenant catalog with
27
39
  no principal-aware policy.
package/README.md CHANGED
@@ -8,6 +8,141 @@ App-runtime MCP server scaffolding for s-m-r-t apps. Provides:
8
8
  `mountMcpToolsRoute` / `mountMcpCallRoute` aliases remain available for one
9
9
  release while applications migrate.
10
10
 
11
+ ## One-call app route
12
+
13
+ Most applications need no server helper module. `mountMcpAppRoute` builds the
14
+ server from the app's own declared models, maps the request principal from the
15
+ SvelteKit session locals populated by `createSessionHandler`
16
+ (`@happyvertical/smrt-users/sveltekit`), and mounts the stateless endpoint:
17
+
18
+ ```ts
19
+ // src/routes/api/mcp/+server.ts
20
+ import { mountMcpAppRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
21
+ import { Item } from '$lib/objects/Item';
22
+ // The app's `createSmrtSvelteKitRuntime()` (@happyvertical/smrt-app-runtime/sveltekit).
23
+ import { runtime } from '$lib/server/smrt';
24
+
25
+ export const POST = mountMcpAppRoute({
26
+ models: [Item],
27
+ requiredScopes: ['items.read'],
28
+ // Per request: under RLS this is the request's transaction-bound database.
29
+ smrtOptions: () => ({ db: runtime.databaseConfig() }),
30
+ });
31
+ ```
32
+
33
+ - **Allow-list** — exactly the `models` listed (registered `@smrt()`
34
+ constructors; anything else throws at construction). Other registered
35
+ models are never enumerated; direct calls to them return the unknown-tool
36
+ error (404 semantics).
37
+ - **Principal** — `principalFromSessionLocals` reads only `locals.user.id`, the
38
+ session-authorized `locals.tenantId`, and `locals.permissions` (as sorted
39
+ `scopes`, `kind: 'human'`). Missing or malformed fields mean unauthenticated.
40
+ URL-selected tenants, headers and bodies are never identity inputs. Override
41
+ with `resolvePrincipal`.
42
+ - **Policy** — `requiredScopes` is required: every authenticated principal must
43
+ be an accepted kind (`principalKinds`, default `['human']`) with an id, a
44
+ tenant, and every listed scope, for every tool and resource. Unauthenticated
45
+ callers keep the base rule (only `publicToolPatterns` read-only tools; none by
46
+ default) and mutating tools always need a principal. A supplied `toolPolicy`
47
+ or `resourcePolicy` is composed with this default and can only narrow it;
48
+ for a wider policy, call `createMcpAppServer` and `mountMcpRoute` directly.
49
+ Because the default is principal-aware, `tools/list` stays `private` even
50
+ with a public-cache attestation. A scope applies uniformly to every
51
+ published tool, so a read scope such as `items.read` must be paired with
52
+ `effects: ['read']` (below) unless the app adds per-operation policy.
53
+ - **Effects** — `effects` (also on `createMcpAppServer`) limits the catalog to
54
+ tools whose effect is listed: `'read'`, `'write'`, `'destructive'`, the
55
+ WebMCP vocabulary. `mcpToolEffect` classifies with the existing read-only
56
+ detection (canonical `readOnlyHint`, else the `_list`/`_get` name rule); any
57
+ other tool is `destructive` unless it declares `destructiveHint: false`.
58
+ Excluded tools are absent from `tools/list`, and a direct call gets the same
59
+ unknown-tool error as a nonexistent name, so they cannot be enumerated.
60
+ Omitted, every allow-listed tool is published (unchanged behaviour).
61
+ Durable task lifecycle calls (`tasks/get`, `tasks/update`, `tasks/cancel`)
62
+ re-apply the allow-list and `effects` filter to the task's originating
63
+ action, the generated tool for the class (simple or qualified name) and
64
+ method its job persists; a task whose action is excluded, or maps to no
65
+ allow-listed generated tool, is answered exactly like an unknown task.
66
+ `toolPolicy` is not re-applied to lifecycle calls: a task created by an
67
+ application workflow does not record which workflow created it, and such
68
+ workflows often front a policy-hidden generated task tool. Owner and tenant
69
+ scoping apply to every lifecycle call. Principal-level authority is checked
70
+ first, before the task store is touched: `createDefaultMcpAppServer` (and so
71
+ `mountMcpAppRoute`) gates every lifecycle call on its principal scope policy
72
+ — accepted kind, id, tenant and every `requiredScopes` entry, evaluated on
73
+ the *effective* bound principal — so a bearer whose live permissions were
74
+ revoked cannot read, resume (supply input to) or cancel an existing task;
75
+ restoring the permission restores access. A custom `createMcpAppServer`
76
+ supplies the same gate with `taskPrincipalPolicy({ principal })`; without
77
+ it, a custom `toolPolicy` is not applied to lifecycle calls. A denial or a
78
+ thrown predicate is answered as an unknown task.
79
+ - **Server** — `serverInfo` defaults to `{ name: 'smrt-app', version: '0.1.0' }`;
80
+ every other `createMcpAppServer` option (`workflowTools`, `resources`,
81
+ `workflowAssertions`, `toolListCache`, …) passes through. The handler's
82
+ `server` property exposes the policy core. The same defaults are available
83
+ without SvelteKit as `createDefaultMcpAppServer` from the root entry.
84
+
85
+ - **Origin check** — on by default (`checkOrigin`). Before bearer
86
+ authentication, principal resolution or any dispatch, a request whose
87
+ `Origin` is present but is not the request URL's own origin (exact scheme,
88
+ host and port) or one of `trustedOrigins` — including `Origin: null` and
89
+ sibling subdomains — gets HTTP 403 with the JSON-RPC error data
90
+ `{ code: 'mcp_origin_denied', retryable: false }`, naming no origins. With
91
+ no `Origin`, a `Sec-Fetch-Site` of `cross-site`/`same-site` is also
92
+ refused. Clients that send no browser origin signals (server-side MCP
93
+ clients, the `smrt-app-cli` bridge) are unaffected; a browser-hosted MCP
94
+ client must be listed in `trustedOrigins`. Bearer requests are checked too:
95
+ the token is not ambient authority, but the MCP Streamable HTTP transport
96
+ requires origin validation and server-side remote clients send no `Origin`.
97
+ "Own origin" is `event.url`, so behind a rewriting proxy configure the
98
+ SvelteKit adapter's origin (adapter-node `ORIGIN`, or its
99
+ `PROTOCOL_HEADER`/`HOST_HEADER`) or add the public origin to
100
+ `trustedOrigins`; this package never reads `X-Forwarded-*`.
101
+ `checkOrigin: false` opts out. `mountMcpRoute` accepts the same options,
102
+ off unless set.
103
+
104
+ Bearer authentication for hosted profiles is an `auth` option on both
105
+ `mountMcpRoute` and `mountMcpAppRoute`. When its source yields an adapter,
106
+ every request must carry a valid bearer token and the adapter's principal
107
+ replaces the session principal; `null` (the `local` profile) keeps the session
108
+ principal. Pass `bindPrincipal` (for example `runtime.runAsPrincipal` from
109
+ `@happyvertical/smrt-app-runtime/sveltekit`) so that, after bearer
110
+ authentication, the `smrtOptions` database thunk, task handling and tool
111
+ execution all run inside a context bound to that principal: under
112
+ `database-rls` the runtime opens a fresh RLS transaction publishing the bearer
113
+ user, tenant and its live membership permissions capped by the token's scopes,
114
+ instead of the anonymous (or a cookie user's) request transaction. The binder
115
+ hands dispatch the principal's effective scopes (`runAsPrincipal` passes the
116
+ token scopes still granted by live permissions), and the tool policy
117
+ authorizes with those, intersected with the token's: a revoked permission
118
+ denies the tool before dispatch even under `application` isolation, while the
119
+ principal keeps the tools its remaining permissions allow. A binder can never
120
+ widen scopes or change the principal's id or tenant. A binder
121
+ that fails before dispatch answers HTTP 403 with the safe
122
+ `mcp_tool_access_denied` error; the response is materialized inside the
123
+ binding. `createHostedMcpResourceAuth` from `./auth` builds that source from
124
+ `SMRT_MCP_RESOURCE`, `SMRT_MCP_ISSUER`, `SMRT_MCP_JWKS_URI` and
125
+ `SMRT_MCP_SCOPES`, caching one adapter and retrying a failed construction:
126
+
127
+ ```ts
128
+ import { createHostedMcpResourceAuth } from '@happyvertical/smrt-app-mcp/auth';
129
+ const auth = createHostedMcpResourceAuth({
130
+ profile: () => applicationRuntime.profile,
131
+ resolvePrincipal: resolveHostedMcpPrincipal, // application-owned lookup
132
+ });
133
+ // api/mcp/+server.ts
134
+ export const POST = mountMcpAppRoute({
135
+ models: [Item], requiredScopes: ['items.read'], effects: ['read'],
136
+ smrtOptions, auth, bindPrincipal: runtime.runAsPrincipal,
137
+ });
138
+ // .well-known/oauth-protected-resource/api/mcp/+server.ts
139
+ export const GET = mountMcpProtectedResourceMetadataRoute(auth);
140
+ ```
141
+
142
+ `mountMcpProtectedResourceMetadataRoute` serves the RFC 9728 document only at
143
+ the adapter's advertised `metadataUrl` path and returns 404 otherwise or for
144
+ the `local` profile. It does not implement an OAuth authorization server.
145
+
11
146
  For piping a deployed app's MCP surface to a local stdio MCP client, see `@happyvertical/smrt-app-cli` — the client-side runtime CLI exposes a `startMcpBridge()` default and a generic `smrt-mcp-bridge` bin.
12
147
 
13
148
  For public deployments, follow the
package/dist/auth.d.ts CHANGED
@@ -49,5 +49,44 @@ export interface McpResourceAuth {
49
49
  response: Response;
50
50
  }>;
51
51
  }
52
+ /** Options for {@link createHostedMcpResourceAuth}. */
53
+ export interface HostedMcpResourceAuthOptions {
54
+ /**
55
+ * Deployment profile, or a thunk read on every call. `local` disables bearer
56
+ * authentication; the route then uses its session principal.
57
+ */
58
+ profile: McpDeploymentProfile | (() => McpDeploymentProfile);
59
+ /**
60
+ * Application-owned mapping from a verified identity to a current account
61
+ * and active membership. Required for every non-local profile; token claims
62
+ * never select a tenant. An absent binding throws on first hosted use.
63
+ */
64
+ resolvePrincipal?: McpResourceAuthOptions['resolvePrincipal'];
65
+ /** Environment source. Defaults to `process.env`, read lazily. */
66
+ env?: Readonly<Record<string, string | undefined>>;
67
+ /** Default `['RS256']`. */
68
+ algorithms?: McpResourceAuthOptions['algorithms'];
69
+ tokenType?: McpResourceAuthOptions['tokenType'];
70
+ requireTenant?: McpResourceAuthOptions['requireTenant'];
71
+ }
72
+ /** Environment variables read by {@link createHostedMcpResourceAuth}. */
73
+ export declare const HOSTED_MCP_AUTH_ENV: Readonly<{
74
+ resource: "SMRT_MCP_RESOURCE";
75
+ issuer: "SMRT_MCP_ISSUER";
76
+ jwksUri: "SMRT_MCP_JWKS_URI";
77
+ scopes: "SMRT_MCP_SCOPES";
78
+ }>;
79
+ /**
80
+ * Lazily build the hosted protected-resource adapter from operator
81
+ * environment. The returned thunk yields `null` for the `local` profile,
82
+ * otherwise one cached `createMcpResourceAuth` instance configured from
83
+ * `SMRT_MCP_RESOURCE`, `SMRT_MCP_ISSUER`, `SMRT_MCP_JWKS_URI` and the
84
+ * whitespace-separated `SMRT_MCP_SCOPES`. Missing configuration throws (fail
85
+ * closed); a failed construction is not cached, so a corrected environment
86
+ * is picked up on the next request. Pass the thunk as the `auth` option of
87
+ * `mountMcpRoute`/`mountMcpAppRoute` and to
88
+ * `mountMcpProtectedResourceMetadataRoute`.
89
+ */
90
+ export declare function createHostedMcpResourceAuth(options: HostedMcpResourceAuthOptions): () => McpResourceAuth | null;
52
91
  export declare function createMcpResourceAuth(options: McpResourceAuthOptions): McpResourceAuth;
53
92
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,EAAsB,KAAK,UAAU,EAAa,MAAM,MAAM,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,aAAa,GAAG,OAAO,CAAC;AACrE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;CACvC;AACD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AACD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC;IAC3C,qFAAqF;IACrF,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;IAC7B,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CACd,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;CACxC;AAqCD,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gBAAgB,IAAI,QAAQ,CAAC;IAC7B,8EAA8E;IAC9E,YAAY,CACV,OAAO,EAAE,OAAO,GACf,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,eAAe,GAAG;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GACzD;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CACpC,CAAC;CACH;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CA0HjB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,EAAsB,KAAK,UAAU,EAAa,MAAM,MAAM,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,aAAa,GAAG,OAAO,CAAC;AACrE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;CACvC;AACD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AACD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,UAAU,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,EAAE,CAAC;IAC3C,qFAAqF;IACrF,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;IAC7B,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,gBAAgB,CACd,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;CACxC;AAqCD,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gBAAgB,IAAI,QAAQ,CAAC;IAC7B,8EAA8E;IAC9E,YAAY,CACV,OAAO,EAAE,OAAO,GACf,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,eAAe,GAAG;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GACzD;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CACpC,CAAC;CACH;AAED,uDAAuD;AACvD,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,OAAO,EAAE,oBAAoB,GAAG,CAAC,MAAM,oBAAoB,CAAC,CAAC;IAC7D;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IAC9D,kEAAkE;IAClE,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IACnD,2BAA2B;IAC3B,UAAU,CAAC,EAAE,sBAAsB,CAAC,YAAY,CAAC,CAAC;IAClD,SAAS,CAAC,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC;IAChD,aAAa,CAAC,EAAE,sBAAsB,CAAC,eAAe,CAAC,CAAC;CACzD;AAED,yEAAyE;AACzE,eAAO,MAAM,mBAAmB;;;;;EAK9B,CAAC;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,4BAA4B,GACpC,MAAM,eAAe,GAAG,IAAI,CAqC9B;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CA0HjB"}
package/dist/auth.js CHANGED
@@ -717,6 +717,39 @@ function validateUrl(value, profile) {
717
717
  function validString(value) {
718
718
  return typeof value === "string" && value.length > 0 && value.length <= 1024 && value.trim() === value && [...value].every((character) => character.charCodeAt(0) > 31 && character.charCodeAt(0) !== 127);
719
719
  }
720
+ var HOSTED_MCP_AUTH_ENV = Object.freeze({
721
+ resource: "SMRT_MCP_RESOURCE",
722
+ issuer: "SMRT_MCP_ISSUER",
723
+ jwksUri: "SMRT_MCP_JWKS_URI",
724
+ scopes: "SMRT_MCP_SCOPES"
725
+ });
726
+ function createHostedMcpResourceAuth(options) {
727
+ let cached;
728
+ return () => {
729
+ const profile = typeof options.profile === "function" ? options.profile() : options.profile;
730
+ if (profile === "local") return null;
731
+ if (cached) return cached;
732
+ const env = options.env ?? globalThis.process?.env ?? {};
733
+ const required = (name) => {
734
+ const value = env[name]?.trim();
735
+ if (!value) throw new Error(`Hosted MCP requires ${name}.`);
736
+ return value;
737
+ };
738
+ if (!options.resolvePrincipal) throw new Error("Hosted MCP requires an application-owned resolvePrincipal binding.");
739
+ cached = createMcpResourceAuth({
740
+ profile,
741
+ resource: required(HOSTED_MCP_AUTH_ENV.resource),
742
+ issuer: required(HOSTED_MCP_AUTH_ENV.issuer),
743
+ jwksUri: required(HOSTED_MCP_AUTH_ENV.jwksUri),
744
+ scopes: required(HOSTED_MCP_AUTH_ENV.scopes).split(/\s+/u),
745
+ algorithms: options.algorithms ?? ["RS256"],
746
+ tokenType: options.tokenType,
747
+ requireTenant: options.requireTenant,
748
+ resolvePrincipal: options.resolvePrincipal
749
+ });
750
+ return cached;
751
+ };
752
+ }
720
753
  function createMcpResourceAuth(options) {
721
754
  if (![
722
755
  "local",
@@ -810,6 +843,6 @@ function createMcpResourceAuth(options) {
810
843
  };
811
844
  }
812
845
  //#endregion
813
- export { createMcpResourceAuth };
846
+ export { HOSTED_MCP_AUTH_ENV, createHostedMcpResourceAuth, createMcpResourceAuth };
814
847
 
815
848
  //# sourceMappingURL=auth.js.map