Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/cloud/src/mcp/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ const propsForPrincipal = (
elicitationMode: readElicitationMode(request),
resource,
webOrigin: new URL(request.url).origin,
// The whole verified caller, so the session DO rebuilds its runtime
// with the same identity the in-memory store passes to buildServer.
principal,
},
propagation,
};
Expand Down
3 changes: 3 additions & 0 deletions apps/host-cloudflare/src/mcp/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ const propsForPrincipal = (
// resource.
resource: defaultMcpResource,
webOrigin: new URL(request.url).origin,
// The whole verified caller, so the session DO rebuilds its runtime
// with the same identity the in-memory store passes to buildServer.
principal,
},
propagation,
};
Expand Down
19 changes: 14 additions & 5 deletions packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import {
PAUSED_APPROVAL_TIMEOUT_MS,
type PausedExecutionHooks,
} from "@executor-js/host-mcp/tool-server";
import { defaultMcpResource, type McpResource } from "@executor-js/host-mcp";
import { defaultMcpResource, type McpResource, type Principal } from "@executor-js/host-mcp";

import type { IncomingPropagationHeaders, McpElicitationMode } from "./do-headers";
import {
SESSION_TIMEOUT_MS,
decideSessionAlarm,
pausedLeaseExtensionLog,
} from "./session-alarm-policy";
import { carrySessionInit } from "./session-meta";

export type IncomingTraceHeaders = IncomingPropagationHeaders;

Expand All @@ -33,6 +34,11 @@ export interface McpSessionInit {
* `/mcp/toolkits/<slug>` toolkit), so the tool catalog is scoped to it. */
readonly resource: McpResource;
readonly webOrigin?: string;
/** The verified caller, carried whole so the DO can rebuild its runtime with
* the same identity the in-memory session store already passes to
* `buildServer`. Optional and additive: unset changes no behavior, and
* ownership validation stays keyed on userId + organizationId. */
readonly principal?: Principal;
}

export interface McpSessionProps extends Record<string, unknown> {
Expand Down Expand Up @@ -89,6 +95,12 @@ export interface SessionMeta {
* `buildMcpServer` scopes the tool catalog to it. */
readonly resource: McpResource;
readonly webOrigin?: string;
/** The verified caller (carried from {@link McpSessionInit}), persisted with
* the meta so a cold isolate rebuilds the runtime with the same identity.
* A snapshot at session mint, same lifetime semantics as userId and
* organizationId (a session does not re-authenticate). Optional; ownership
* validation still keys on userId + organizationId only. */
readonly principal?: Principal;
}

export interface BuiltMcpServer {
Expand Down Expand Up @@ -341,10 +353,7 @@ export abstract class McpAgentSessionDOBase<
const self = this;
return Effect.gen(function* () {
const resolved = yield* self.resolveSessionMeta(token);
const sessionMeta: SessionMeta = {
...resolved,
...(token.webOrigin ? { webOrigin: token.webOrigin } : {}),
};
const sessionMeta = carrySessionInit(resolved, token);
yield* Effect.promise(() => self.saveSessionMeta(sessionMeta)).pipe(
Effect.withSpan("mcp.session.save_meta"),
);
Expand Down
54 changes: 54 additions & 0 deletions packages/hosts/cloudflare/src/mcp/session-meta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "@effect/vitest";

import type { Principal } from "@executor-js/host-mcp";

import type { SessionMeta } from "./agent-session-durable-object";
import { carrySessionInit } from "./session-meta";

const resolved: SessionMeta = {
organizationId: "org-1",
organizationName: "Org One",
userId: "acct-1",
elicitationMode: "model",
resource: { kind: "default" },
};

const principal: Principal = {
accountId: "acct-1",
organizationId: "org-1",
organizationName: "Org One",
email: "person@example.com",
name: "Person",
avatarUrl: null,
roles: ["admin"],
};

describe("carrySessionInit", () => {
it("carries the verified principal onto the stored meta", () => {
const meta = carrySessionInit(resolved, { principal });
expect(meta.principal).toEqual(principal);
});

it("carries webOrigin and principal together", () => {
const meta = carrySessionInit(resolved, {
webOrigin: "https://executor.example.com",
principal,
});
expect(meta.webOrigin).toBe("https://executor.example.com");
expect(meta.principal).toEqual(principal);
});

it("leaves both unset when the init carries neither", () => {
const meta = carrySessionInit(resolved, {});
expect(meta).toEqual(resolved);
expect("principal" in meta).toBe(false);
expect("webOrigin" in meta).toBe(false);
});

it("does not let carried fields clobber host-resolved meta", () => {
const meta = carrySessionInit(resolved, { principal });
expect(meta.organizationId).toBe("org-1");
expect(meta.userId).toBe("acct-1");
expect(meta.resource).toEqual({ kind: "default" });
});
});
17 changes: 17 additions & 0 deletions packages/hosts/cloudflare/src/mcp/session-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { McpSessionInit, SessionMeta } from "./agent-session-durable-object";

/**
* Merge the host-resolved meta with the fields the base carries verbatim from
* `McpSessionInit`: `webOrigin` and the verified `principal`. Carrying them
* here (rather than in every host's `resolveSessionMeta`) keeps each host's
* resolver lossless without it knowing about either field, and mirrors what
* the in-memory session store gives `buildServer`: the whole principal.
*/
export const carrySessionInit = (
resolved: SessionMeta,
token: Pick<McpSessionInit, "webOrigin" | "principal">,
): SessionMeta => ({
...resolved,
...(token.webOrigin ? { webOrigin: token.webOrigin } : {}),
...(token.principal ? { principal: token.principal } : {}),
});