holon-sh 0.0.1-dev.15 → 0.0.1-dev.16

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/README.md CHANGED
@@ -134,6 +134,38 @@ await serve(await import("./server.ts"));
134
134
 
135
135
  The MCP endpoint is at `/mcp` and `/mcp/<app>`, and the files of `frontend` are at every other path. `PORT` and `HOST` change where it listens, 127.0.0.1:3000 unless they are set. Holon answers clients of the 2025 MCP versions in front of an app, and here the app's handler answers them, so create it without `legacy: "reject"`.
136
136
 
137
+ ## The caller and the origin
138
+
139
+ `getContext` reads who called a tool, and where the app was reached, so a link the app makes works wherever it runs:
140
+
141
+ ```ts
142
+ import { getContext } from "holon-sh/context";
143
+
144
+ server.registerTool("share", { description: "Links to the page" }, ctx => {
145
+ const { principal, origin } = getContext(ctx.http);
146
+ return { content: [{ type: "text", text: `${origin}/?by=${principal?.subject}` }] };
147
+ });
148
+ ```
149
+
150
+ `principal` is the user Holon admitted, or null. `origin` is the one Holon or a proxy forwards, or else the one the request reached.
151
+
152
+ ## A page in MCP Apps hosts
153
+
154
+ `holon-sh/page` offers an app's page, built as one HTML file, as the view an MCP Apps host shows with a tool's result:
155
+
156
+ ```ts
157
+ import { createPage } from "holon-sh/page";
158
+
159
+ const page = createPage(new URL("./dist/mcp/index.html", import.meta.url));
160
+
161
+ function createServer() {
162
+ const server = new McpServer({ name: "foo", version: "0.1.0" });
163
+ server.registerTool("open", { description: "Opens the page", _meta: page.meta }, open);
164
+ page.serve(server);
165
+ return server;
166
+ }
167
+ ```
168
+
137
169
  ## Community
138
170
 
139
171
  Ask questions and share what you're building on the [Holon Discord](https://discord.gg/jXExyBdy).
package/dist/context.d.ts CHANGED
@@ -40,16 +40,19 @@ export interface Principal {
40
40
  claims: IdentityClaims;
41
41
  actor?: Record<string, unknown>;
42
42
  }
43
- /** The caller of the current MCP request. principal is null for anonymous requests. */
43
+ /** The caller of the current MCP request, and the origin it reached the app at. principal is null for anonymous requests, and origin without HTTP. */
44
44
  export interface RequestContext {
45
45
  principal: Principal | null;
46
+ origin: string | null;
46
47
  }
47
- /** Read the caller from the context an MCP server receives: a tool callback's ctx.http, or the McpRequestContext of a createMcpHandler factory. */
48
+ /** Read the caller and the app's origin from the context an MCP server receives: a tool callback's ctx.http, or the McpRequestContext of a createMcpHandler factory. */
48
49
  export declare function getContext(context?: {
49
50
  authInfo?: {
50
51
  clientId: string;
51
52
  scopes: string[];
52
53
  extra?: Record<string, unknown>;
53
54
  };
55
+ req?: Request;
56
+ requestInfo?: Request;
54
57
  }): RequestContext;
55
58
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IACxH,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AACD,yHAAyH;AACzH,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAClG,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IACrG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AACD,yDAAyD;AACzD,MAAM,WAAW,SAAS;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE;AAC3J,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAAG,SAAS,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE;AAM/D,mJAAmJ;AACnJ,wBAAgB,UAAU,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CAAO,GAAG,cAAc,CAK/I"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IACxH,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AACD,yHAAyH;AACzH,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClG,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAClG,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAClG,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IACrG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AACD,yDAAyD;AACzD,MAAM,WAAW,SAAS;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE;AAC3J,sJAAsJ;AACtJ,MAAM,WAAW,cAAc;IAAG,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAMtF,wKAAwK;AACxK,wBAAgB,UAAU,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,cAAc,CAMrL"}
package/dist/context.js CHANGED
@@ -1,9 +1,15 @@
1
- /** Read the caller from the context an MCP server receives: a tool callback's ctx.http, or the McpRequestContext of a createMcpHandler factory. */
1
+ /** Read the caller and the app's origin from the context an MCP server receives: a tool callback's ctx.http, or the McpRequestContext of a createMcpHandler factory. */
2
2
  export function getContext(context = {}) {
3
- const { authInfo } = context;
3
+ const { authInfo, req = context.requestInfo } = context;
4
4
  const { iss, sub, claims, act } = (authInfo?.extra ?? {});
5
- if (!authInfo || typeof iss !== "string" || typeof sub !== "string")
6
- return { principal: null };
7
- return { principal: { issuer: iss, subject: sub, clientId: authInfo.clientId, scopes: authInfo.scopes, claims: claims ?? {}, ...(act && { actor: act }) } };
5
+ const principal = !authInfo || typeof iss !== "string" || typeof sub !== "string" ? null
6
+ : { issuer: iss, subject: sub, clientId: authInfo.clientId, scopes: authInfo.scopes, claims: claims ?? {}, ...(act && { actor: act }) };
7
+ return { principal, origin: req ? origin(req) : null };
8
+ }
9
+ // Holon forwards the origin it serves the app at, as a proxy in front may. Otherwise the request reached the app where it
10
+ // listens.
11
+ function origin({ url, headers }) {
12
+ const { protocol, host } = new URL(url);
13
+ return `${headers.get("x-forwarded-proto") ?? protocol.slice(0, -1)}://${headers.get("x-forwarded-host") ?? host}`;
8
14
  }
9
15
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAsBA,mJAAmJ;AACnJ,MAAM,UAAU,UAAU,CAAC,OAAO,GAA2F,EAAE;IAC7H,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAC7B,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAa,CAAC;IACtE,IAAI,CAAC,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAChG,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;AAC9J,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAsBA,wKAAwK;AACxK,MAAM,UAAU,UAAU,CAAC,OAAO,GAAiI,EAAE;IACnK,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC;IACxD,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAa,CAAC;IACtE,MAAM,SAAS,GAAG,CAAC,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI;QACtF,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IAC1I,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACzD,CAAC;AAED,0HAA0H;AAC1H,WAAW;AACX,SAAS,MAAM,CAAC,EAAE,GAAG,EAAE,OAAO,EAAW;IACvC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,IAAI,EAAE,CAAC;AACrH,CAAC"}
package/dist/page.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server";
2
+ /** An app's page, as the view an MCP Apps host shows with a tool's result. */
3
+ export interface Page {
4
+ /** A tool's `_meta`, so that hosts show the page with what the tool returns. */
5
+ meta: {
6
+ ui: {
7
+ resourceUri: string;
8
+ };
9
+ };
10
+ /** Offers the page to a server's clients as its view. */
11
+ serve(server: Pick<McpServer, "registerResource">): void;
12
+ }
13
+ /**
14
+ * Makes an app's page a view: `file` is the page built as one HTML file, and `ui` the view's `_meta.ui`, such as its
15
+ * `csp`.
16
+ *
17
+ * ```ts
18
+ * const page = createPage(new URL("../dist/mcp/index.html", import.meta.url));
19
+ * server.registerTool("open", { description: "Opens it", _meta: page.meta }, open);
20
+ * page.serve(server);
21
+ * ```
22
+ */
23
+ export declare function createPage(file: URL, ui?: Record<string, unknown>): Page;
24
+ //# sourceMappingURL=page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAQ9D,8EAA8E;AAC9E,MAAM,WAAW,IAAI;IACnB,gFAAgF;IAChF,IAAI,EAAE;QAAE,EAAE,EAAE;YAAE,WAAW,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACtC,yDAAyD;IACzD,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI,CAQ5E"}
package/dist/page.js ADDED
@@ -0,0 +1,25 @@
1
+ import { readFile } from "node:fs/promises";
2
+ // MCP Apps names a view with a ui:// URI and serves it as this type. An app has one page, so the URI names no app and
3
+ // survives renaming one.
4
+ // https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
5
+ const uri = "ui://page";
6
+ const mimeType = "text/html;profile=mcp-app";
7
+ /**
8
+ * Makes an app's page a view: `file` is the page built as one HTML file, and `ui` the view's `_meta.ui`, such as its
9
+ * `csp`.
10
+ *
11
+ * ```ts
12
+ * const page = createPage(new URL("../dist/mcp/index.html", import.meta.url));
13
+ * server.registerTool("open", { description: "Opens it", _meta: page.meta }, open);
14
+ * page.serve(server);
15
+ * ```
16
+ */
17
+ export function createPage(file, ui = {}) {
18
+ return {
19
+ meta: { ui: { resourceUri: uri } },
20
+ serve: server => void server.registerResource("Page", uri, { mimeType }, async () => ({
21
+ contents: [{ uri, mimeType, text: await readFile(file, "utf8"), _meta: { ui } }],
22
+ })),
23
+ };
24
+ }
25
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5C,sHAAsH;AACtH,yBAAyB;AACzB,+FAA+F;AAC/F,MAAM,GAAG,GAAG,WAAW,CAAC;AACxB,MAAM,QAAQ,GAAG,2BAA2B,CAAC;AAU7C;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,IAAS,EAAE,EAAE,GAA4B,EAAE;IACpE,OAAO;QACL,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE;QAClC,KAAK,EAAE,MAAM,CAAC,EAAE,CACd,KAAK,MAAM,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;YACnE,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;SACjF,CAAC,CAAC;KACN,CAAC;AACJ,CAAC"}
package/dist/serve.js CHANGED
@@ -41,11 +41,6 @@ async function respond(handler, directory, req, res) {
41
41
  for (const [name, values = []] of Object.entries(req.headersDistinct))
42
42
  for (const value of values)
43
43
  headers.append(name, value);
44
- // Holon forwards the origin it serves the app at. Here that is this server, unless a proxy in front forwards its own.
45
- if (!headers.has("x-forwarded-host"))
46
- headers.set("x-forwarded-host", url.host);
47
- if (!headers.has("x-forwarded-proto"))
48
- headers.set("x-forwarded-proto", "http");
49
44
  const body = req.method === "GET" || req.method === "HEAD" ? undefined : req;
50
45
  const response = await handler.fetch(new Request(url, { method: req.method, headers, body, duplex: "half", signal: gone.signal }));
51
46
  res.setHeaders(response.headers).writeHead(response.status);
package/dist/serve.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "holon-sh",
3
- "version": "0.0.1-dev.15",
3
+ "version": "0.0.1-dev.16",
4
4
  "description": "Typed MCP clients, MCP Events, and the caller of an MCP request, for Node.js.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,6 +39,10 @@
39
39
  "./serve": {
40
40
  "types": "./dist/serve.d.ts",
41
41
  "import": "./dist/serve.js"
42
+ },
43
+ "./page": {
44
+ "types": "./dist/page.d.ts",
45
+ "import": "./dist/page.js"
42
46
  }
43
47
  },
44
48
  "typesVersions": {
@@ -60,6 +64,9 @@
60
64
  ],
61
65
  "serve": [
62
66
  "./dist/serve.d.ts"
67
+ ],
68
+ "page": [
69
+ "./dist/page.d.ts"
63
70
  ]
64
71
  }
65
72
  },
package/src/context.ts CHANGED
@@ -13,17 +13,25 @@ export interface IdentityClaims {
13
13
  }
14
14
  /** The user a request acts for, as Holon admitted it. */
15
15
  export interface Principal { issuer: string; subject: string; clientId: string; scopes: string[]; claims: IdentityClaims; actor?: Record<string, unknown> }
16
- /** The caller of the current MCP request. principal is null for anonymous requests. */
17
- export interface RequestContext { principal: Principal | null }
16
+ /** The caller of the current MCP request, and the origin it reached the app at. principal is null for anonymous requests, and origin without HTTP. */
17
+ export interface RequestContext { principal: Principal | null; origin: string | null }
18
18
 
19
19
  // Holon admits the caller before the request reaches the server and hands it over as MCP's authInfo, with claims and
20
20
  // act in these shapes.
21
21
  interface Admitted { iss?: unknown; sub?: unknown; claims?: IdentityClaims; act?: Record<string, unknown> }
22
22
 
23
- /** Read the caller from the context an MCP server receives: a tool callback's ctx.http, or the McpRequestContext of a createMcpHandler factory. */
24
- export function getContext(context: { authInfo?: { clientId: string; scopes: string[]; extra?: Record<string, unknown> } } = {}): RequestContext {
25
- const { authInfo } = context;
23
+ /** Read the caller and the app's origin from the context an MCP server receives: a tool callback's ctx.http, or the McpRequestContext of a createMcpHandler factory. */
24
+ export function getContext(context: { authInfo?: { clientId: string; scopes: string[]; extra?: Record<string, unknown> }; req?: Request; requestInfo?: Request } = {}): RequestContext {
25
+ const { authInfo, req = context.requestInfo } = context;
26
26
  const { iss, sub, claims, act } = (authInfo?.extra ?? {}) as Admitted;
27
- if (!authInfo || typeof iss !== "string" || typeof sub !== "string") return { principal: null };
28
- return { principal: { issuer: iss, subject: sub, clientId: authInfo.clientId, scopes: authInfo.scopes, claims: claims ?? {}, ...(act && { actor: act }) } };
27
+ const principal = !authInfo || typeof iss !== "string" || typeof sub !== "string" ? null
28
+ : { issuer: iss, subject: sub, clientId: authInfo.clientId, scopes: authInfo.scopes, claims: claims ?? {}, ...(act && { actor: act }) };
29
+ return { principal, origin: req ? origin(req) : null };
30
+ }
31
+
32
+ // Holon forwards the origin it serves the app at, as a proxy in front may. Otherwise the request reached the app where it
33
+ // listens.
34
+ function origin({ url, headers }: Request) {
35
+ const { protocol, host } = new URL(url);
36
+ return `${headers.get("x-forwarded-proto") ?? protocol.slice(0, -1)}://${headers.get("x-forwarded-host") ?? host}`;
29
37
  }
package/src/page.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import type { McpServer } from "@modelcontextprotocol/server";
3
+
4
+ // MCP Apps names a view with a ui:// URI and serves it as this type. An app has one page, so the URI names no app and
5
+ // survives renaming one.
6
+ // https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
7
+ const uri = "ui://page";
8
+ const mimeType = "text/html;profile=mcp-app";
9
+
10
+ /** An app's page, as the view an MCP Apps host shows with a tool's result. */
11
+ export interface Page {
12
+ /** A tool's `_meta`, so that hosts show the page with what the tool returns. */
13
+ meta: { ui: { resourceUri: string } };
14
+ /** Offers the page to a server's clients as its view. */
15
+ serve(server: Pick<McpServer, "registerResource">): void;
16
+ }
17
+
18
+ /**
19
+ * Makes an app's page a view: `file` is the page built as one HTML file, and `ui` the view's `_meta.ui`, such as its
20
+ * `csp`.
21
+ *
22
+ * ```ts
23
+ * const page = createPage(new URL("../dist/mcp/index.html", import.meta.url));
24
+ * server.registerTool("open", { description: "Opens it", _meta: page.meta }, open);
25
+ * page.serve(server);
26
+ * ```
27
+ */
28
+ export function createPage(file: URL, ui: Record<string, unknown> = {}): Page {
29
+ return {
30
+ meta: { ui: { resourceUri: uri } },
31
+ serve: server =>
32
+ void server.registerResource("Page", uri, { mimeType }, async () => ({
33
+ contents: [{ uri, mimeType, text: await readFile(file, "utf8"), _meta: { ui } }],
34
+ })),
35
+ };
36
+ }
package/src/serve.ts CHANGED
@@ -50,9 +50,6 @@ async function respond(handler: App["default"], directory: URL | undefined, req:
50
50
  res.on("close", () => gone.abort());
51
51
  const headers = new Headers();
52
52
  for (const [name, values = []] of Object.entries(req.headersDistinct)) for (const value of values) headers.append(name, value);
53
- // Holon forwards the origin it serves the app at. Here that is this server, unless a proxy in front forwards its own.
54
- if (!headers.has("x-forwarded-host")) headers.set("x-forwarded-host", url.host);
55
- if (!headers.has("x-forwarded-proto")) headers.set("x-forwarded-proto", "http");
56
53
  const body = req.method === "GET" || req.method === "HEAD" ? undefined : req;
57
54
  const response = await handler.fetch(new Request(url, { method: req.method, headers, body, duplex: "half", signal: gone.signal }));
58
55
  res.setHeaders(response.headers).writeHead(response.status);