Skip to content

feat(core): BaseContext.ext / RequestEnv.ext plugin slot (SEP-2133 mechanism)#2061

Draft
felixweinberger wants to merge 3 commits into
fweinberger/s1-sessionid-optfrom
fweinberger/s2-ext-slot
Draft

feat(core): BaseContext.ext / RequestEnv.ext plugin slot (SEP-2133 mechanism)#2061
felixweinberger wants to merge 3 commits into
fweinberger/s1-sessionid-optfrom
fweinberger/s2-ext-slot

Conversation

@felixweinberger
Copy link
Copy Markdown
Contributor


Adds ext?: Record<string, unknown> to BaseContext and RequestEnv — a generic slot plugins/drivers populate and handlers read via a typed helper. Core never reads or writes it.

Motivation and Context

The mechanism SEP-2133 extensions use to inject per-request context without core knowing about them. tasksPlugin (F3) puts ext.task; SessionCompat puts ext.sessionId.

How Has This Been Tested?

typecheck + lint clean. Type-only change.

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Depends on S1. Review guide: https://gist.github.com/felixweinberger/5a48e0f14d5aced39ed6a91b61940711.


@felixweinberger felixweinberger added the v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless) label May 12, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

⚠️ No Changeset found

Latest commit: 06673f2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 12, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2061

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2061

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2061

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2061

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2061

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2061

commit: 06673f2

@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

1 similar comment
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread packages/core/src/shared/context.ts Outdated
Comment on lines +188 to +194
* @example
* ```ts
* // In a DispatchMiddleware:
* env.ext = { ...env.ext, 'io.modelcontextprotocol/task': { id, store } };
* // In a handler:
* const task = taskContext(ctx); // typed read of ctx.ext['io.modelcontextprotocol/task']
* ```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit: per CLAUDE.md "JSDoc @example Code Snippets", @example blocks should use ```ts source="./file.examples.ts#region" fences so pnpm sync:snippets keeps them type-checked — every other @example in packages/core/src follows this, but this one is an inline fence referencing symbols (env, id, store, taskContext) that do not exist yet (tasksPlugin ships in F3). Consider either rewriting the example around readExt() (which this PR does export) in a context.examples.ts //#region, or dropping the @example tag until F3 lands so the snippet can be sourced and type-checked.

Extended reasoning...

What this is

CLAUDE.md:44-48 documents the repo's @example convention:

JSDoc @example tags should pull type-checked code from companion .examples.ts files … Use ```ts source="./file.examples.ts#regionName" fences referencing //#region regionName blocks … Run pnpm sync:snippets to sync example content into JSDoc comments and markdown files.

The new @example on BaseContext.ext (context.ts:188-194) uses a bare ```ts fence with no source= attribute and no companion packages/core/src/shared/context.examples.ts file, so sync:snippets can never validate it.

Why it matters here specifically

Grepping packages/core/src for @example shows this is the only un-sourced example in the core package — sdkErrors.ts, index.ts, protocol.ts, specTypeSchema.ts, and every validators/*.ts example all use source="./*.examples.ts#region" with a matching companion file (8 .examples.ts files exist under packages/core/src). So within core the convention is uniformly followed today, and this PR introduces the first divergence.

More substantively, the snippet references env, id, store, and taskContext(ctx). Per the PR description, tasksPlugin / taskContext ship in F3, not this PR — so the example is forward-referencing API that doesn't exist yet and cannot be type-checked today even if a .examples.ts were added. That is exactly the drift the sourced-example convention is meant to prevent: if F3's eventual API differs (different key, different accessor name), this doc comment silently rots.

Step-by-step

  1. CLAUDE.md:46 says @example fences should carry source="./file.examples.ts#regionName".
  2. ls packages/core/src/shared/*.examples.ts → only protocol.examples.ts; no context.examples.ts.
  3. grep -r "@example" packages/core/src → every hit (sdkErrors.ts:46, validators/types.ts:38, validators/cfWorkerProvider.ts:22/27, validators/fromJsonSchema.ts:18, validators/ajvProvider.ts:25/30/46+, index.ts:44/49, shared/protocol.ts) has source= on the next line. context.ts:188 does not.
  4. The snippet calls taskContext(ctx); grep -r "export function taskContext" packages/ → no matches (it's slated for F3).
  5. Therefore the example is un-type-checked pseudocode that pnpm sync:snippets ignores, and it references not-yet-existing API.

Addressing the "there are other inline examples" objection

It's true a handful of inline @example blocks exist elsewhere in the monorepo (packages/server/src/validators/cfWorker.ts:4, packages/middleware/express/src/auth/metadataRouter.ts:143, packages/server/src/experimental/tasks/server.ts:84/181). However: (a) none of those are in packages/core, where the convention is currently 100% consistent, and (b) those stragglers reference real, in-tree symbols, whereas this snippet references API from a future PR. The combination of "first inline example in core" + "references unshipped symbols" is what makes it worth a nit — not the bare style point on its own.

Suggested fix

Either:

  • Rewrite the example around what this PR actually ships, e.g. in packages/core/src/shared/context.examples.ts:
    //#region BaseContext_ext_read
    import { type BaseContext, readExt } from "@modelcontextprotocol/core";
    declare const ctx: BaseContext;
    type TaskExt = { id: string };
    const task = readExt<TaskExt>(ctx, "io.modelcontextprotocol/task");
    //#endregion
    and reference it with ```ts source="./context.examples.ts#BaseContext_ext_read"; or
  • Keep the prose description (which already explains the mechanism well) and drop the @example block until F3 lands taskContext, then add a sourced example.

Severity

nit — this is a documentation-convention issue, not a runtime or type-correctness bug. sync:snippets --check ignores fences without source=, so CI does not fail on it.

@felixweinberger felixweinberger force-pushed the fweinberger/s1-sessionid-opt branch from 6122f03 to 1127824 Compare May 12, 2026 14:38
@felixweinberger felixweinberger force-pushed the fweinberger/s2-ext-slot branch from df45523 to 6bcdf6c Compare May 12, 2026 14:38
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for dropping readExt and its public export — the net diff is now a JSDoc-only expansion on the existing ext field, so LGTM. The un-sourced @example nit still stands but is non-blocking; feel free to swap it for a sourced snippet when F3 lands taskContext.

Extended reasoning...

Overview

After commit 6bcdf6c, this PR's net diff against base touches a single file (packages/core/src/shared/context.ts) and only rewrites the JSDoc comment on the pre-existing BaseContext.ext field — expanding two lines of prose into a fuller description plus an @example block. There are zero runtime or type-signature changes: ext?: Record<string, unknown> was already present on both BaseContext and RequestEnv in the base, and the readExt helper + its exports/public/index.ts re-export that I flagged in the first review round have been removed entirely (grep confirms no remaining references anywhere in packages/, and exports/public/index.ts has no net diff).

Security risks

None. This is a comment-only change with no effect on emitted JS, exported types, transport behavior, auth, or protocol handling.

Level of scrutiny

Low. My earlier 🔴 concern was about public API surface growth (readExt); the author addressed it by deleting the helper outright, which is the stronger of the two fixes I suggested. What remains is documentation on an internal mechanism that core itself never reads or writes. The repo's REVIEW.md API-surface and correctness checklists have nothing left to apply to a JSDoc-only diff.

Other factors

  • The 🟡 nit (inline comment #3226617671) about the @example fence not using source="./context.examples.ts#region" and forward-referencing taskContext (ships in F3) is still applicable, but I flagged it as a nit precisely because sync:snippets --check ignores un-sourced fences and CI does not fail. The author has seen it and can address it when F3 introduces a real taskContext to source from. Not re-raising.
  • No changeset is included, which is appropriate for a doc-only change in a stacked PR (v2-stateless track, depends on S1).
  • Inline comment #3226617653 (the readExt blocker) is marked resolved in the timeline, matching the code state.

@felixweinberger felixweinberger force-pushed the fweinberger/s1-sessionid-opt branch from 1127824 to 0a5b206 Compare May 12, 2026 15:57
@felixweinberger felixweinberger force-pushed the fweinberger/s2-ext-slot branch from 6bcdf6c to 1ccef83 Compare May 12, 2026 15:57
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — thanks for dropping readExt; the net diff is now just the JSDoc on BaseContext.ext. The @example sourcing nit still stands but isn't blocking.

Extended reasoning...

Overview

After revisions, this PR's net diff against base is a single JSDoc comment change in packages/core/src/shared/context.ts: the description on the already-existing BaseContext.ext?: Record<string, unknown> field is expanded from two lines to a fuller explanation referencing SEP-2133 plus an @example block. There are no type changes, no new fields, no new exports, and no runtime code touched. The originally-added readExt helper and its exports/public/index.ts re-export — flagged in my prior review — were removed in commit 02caf53, and a follow-up commit (1ccef83) replaced a {@linkcode Protocol.use} reference (private symbol) with plain use() to keep typedoc clean. Grep confirms zero remaining readExt references in the repo.

Security risks

None. This is a comment-only edit; no executable code, schemas, transports, or auth paths are modified.

Level of scrutiny

Low. With readExt gone, what remains is documentation prose on an existing optional field that core never reads or writes. There is no public-API-surface delta to evaluate against REVIEW.md's minimalism / burden-of-proof principles, and no behavior to test.

Other factors

  • My previous 🔴 concern is fully resolved (inline thread marked resolved; helper deleted rather than just un-exported, which is the cleaner outcome).
  • The outstanding 🟡 nit about the un-sourced @example fence (references taskContext, which ships in F3) is still open inline. It's a documentation-convention point — sync:snippets --check ignores fences without source=, so CI is unaffected — and the author has seen it across two re-review cycles. Reasonable to defer sourcing until F3 lands the referenced symbol; not a blocker.
  • No changeset is included, which is appropriate for a doc-comment-only change on a v2-stateless stacked branch.
  • No specific CODEOWNERS beyond the default team apply to this path.

@felixweinberger felixweinberger force-pushed the fweinberger/s1-sessionid-opt branch from 0a5b206 to 3cbbcd0 Compare May 12, 2026 17:28
@felixweinberger felixweinberger force-pushed the fweinberger/s2-ext-slot branch from 1ccef83 to 06673f2 Compare May 12, 2026 17:28
@felixweinberger
Copy link
Copy Markdown
Contributor Author

@claude review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for dropping readExt and its public export — that resolves the API-surface concern. What remains is a JSDoc-only expansion on BaseContext.ext; the inline @example nit still stands but is non-blocking. LGTM.

Extended reasoning...

Overview

After revisions (commits 6879b2f and 06673f2), the net diff is a single hunk in packages/core/src/shared/context.ts: the JSDoc on BaseContext.ext is expanded from a two-line description to a fuller one referencing SEP-2133, the use() middleware hook, the reverse-DNS key convention, and an illustrative @example. No types change, no fields are added or removed, no exports change. readExt — the one-line cast helper I flagged earlier as an unjustified public-API addition — has been deleted from both context.ts and exports/public/index.ts (grep confirms zero remaining references in the repo), and the inline comment is marked resolved.

Security risks

None. This is a comment-only change with no runtime code, no new exports, and no effect on transports, auth, or protocol handling.

Level of scrutiny

Low. The prior review's substantive concern was the speculative public export; that is now fully addressed by removal rather than demotion to internal, which is the cleaner outcome per REVIEW.md's minimalism principle. The remaining change is documentation prose on an existing optional field that core never reads or writes.

Other factors

The 🟡 nit about the @example not using a source="./context.examples.ts#region" fence (CLAUDE.md § JSDoc @example Code Snippets) and forward-referencing taskContext (which lands in F3) was not addressed. I'm treating that as the non-blocking style point I labeled it as: sync:snippets --check ignores un-sourced fences so CI is unaffected, and the prose above the example already conveys the mechanism clearly. It can be converted to a sourced snippet when F3 lands taskContext. No changeset is included, which is appropriate for a doc-only delta on a stacked feature branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2-stateless 2026-06 SDK: Protocol decomposition + SEP alignment (request-first / stateless)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant