-
Notifications
You must be signed in to change notification settings - Fork 8
Add typed state support to McpServer #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Introduces generic TConfig parameter to McpServer, enabling type-safe state access across middleware and handlers. Maintains backward compatibility with default Record<string, unknown> state type.
🦋 Changeset detectedLatest commit: 6e2ad10 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
|
What about using a // ✅ Chainable, clean inference
const server = new McpServer({ name: "app", version: "1.0.0" })
.withState(() => ({
count: 0,
user: null as string | null
}))
.use((ctx, next) => {
ctx.state.count += 1; // ✅ Typed
await next();
})
.tool("getCount", {
handler: (_, ctx) => ({
content: [{ type: "text", text: String(ctx.state.count) }] // ✅ Typed
})
});
// ✅ Async version
const asyncServer = new McpServer({ name: "app", version: "1.0.0" })
.withStateAsync(async () => {
const config = await loadConfig();
return { config, startTime: Date.now() };
});The code as is right now sets the state to an object but types it as unknown. If the user says a certain property is always set on the object but that hasn't happened yet then you still run into problems, with this pattern the state's type is derived from what the builder function returns. |
| @@ -0,0 +1,7 @@ | |||
| --- | |||
| "mcp-lite": major | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor? not sure if we wanna bump to v1
| "mcp-lite": major | |
| "mcp-lite": minor |
| private mountChild( | ||
| prefix: string, | ||
| suffix: string, | ||
| child: McpServer<TConfig>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thinking about the implication for child/grouped servers. all their Config types have to be compatible?
examples/typed-state-example.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we make this a full fledged example? this is just gonna be a rogue typescript file in the examples dir
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh sneaky claude
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bigger note:
the state example is a little confusing to me. the example suggests it is shared across requests from the examples. but i don't think it is, as this should be created afresh each time on each request, which calls createContext. example could use a little freshening up too as its just a rogue file that doesn't create a servable server (but maybe that's the point)
at any rate, given that this takes an optimistic typing approach, we need some way to seed the state on the request. i think the way you can optionally add authInfo when handling a request would be the correct place, as that's where you're actually creating new context for the jsonrpc req/res.
if you don't seed the request with a properly typed initialState, we should throw a type error.
on top of that at the very least probably wanna:
- update readme in some capacity
- update create-mcp-lite docs added to LLM agents, in case they reference ctx.env
i like the spirit of the builder pattern as it solves the issue of improperly seeded initial state (and the problem with optimistic typing more generally), but then with this example state initialization would get funky. i presume that you would want to initialize state with information pertaining to the incoming request (user info, ratelimit info, etc etc), so the builder would then need to accept some sort of initialization function or init data. then we / the end user needs to properly type the input to the builder and so on. |
|
this is intentionally simple, following Hono's pattern: How it works:
no initialization needed - just declare the interface and set properties in middleware: interface MyState {
userId?: string; // optional: might not be set
startTime: number; // required: auth middleware always sets this
}
const server = new McpServer<{ State: MyState }>({ ... });
server.use(async (ctx, next) => {
ctx.state.startTime = Date.now(); // always set
await next();
});the example's another thought: might need a better name than |
Changes
Adds generic
TConfigparameter toMcpServerclass, enabling type-safe state access in middleware and handlers.Breaking Change
ctx.envproperty has been removed and replaced withctx.state.Migration: Rename all
ctx.envreferences toctx.state.Implementation
McpServer<TConfig>accepts generic config withStatepropertyMCPServerContext<TConfig>threads type through toctx.stateMethodHandler,ToolEntry,PromptEntry,ResourceEntry,ResourceHandler,PromptHandler) propagateTConfigcreateContext<TConfig>()properly types the state objectTConfig = { State: Record<string, unknown> }maintains type flexibilityFiles Modified
packages/core/src/core.ts: AddTConfiggeneric toMcpServerclasspackages/core/src/context.ts: AddTConfiggeneric tocreateContext, removeenvpropertypackages/core/src/types.ts: Update all handler and entry types withTConfig, replaceenvwithstateexamples/typed-state-example.ts: Example demonstrating typed state usageExample