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
5 changes: 5 additions & 0 deletions .changeset/some-buses-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

Add Props type parameter to ExportedHandler for typed ctx.props
11 changes: 11 additions & 0 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ function getNextCronTime(cron: string) {

export type { TransportType } from "./mcp/types";

export type {
ExportedHandler,
ExportedHandlerFetchHandler,
ExportedHandlerTailHandler,
ExportedHandlerTraceHandler,
ExportedHandlerTailStreamHandler,
ExportedHandlerScheduledHandler,
ExportedHandlerQueueHandler,
ExportedHandlerTestHandler
} from "./types";

/**
* MCP Server state update message from server -> Client
*/
Expand Down
22 changes: 14 additions & 8 deletions packages/agents/src/mcp/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,23 @@ export interface CreateMcpHandlerOptions extends WorkerTransportOptions {
transport?: WorkerTransport;
}

export function createMcpHandler(
export function createMcpHandler<
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems like a good improvement independently of the types below.

Env = unknown,
Props = unknown
>(
server: McpServer | Server,
options: CreateMcpHandlerOptions = {}
): (
request: Request,
env: unknown,
ctx: ExecutionContext
env: Env,
ctx: ExecutionContext<Props>
) => Promise<Response> {
const route = options.route ?? "/mcp";

return async (
request: Request,
_env: unknown,
ctx: ExecutionContext
_env: Env,
ctx: ExecutionContext<Props>
): Promise<Response> => {
const url = new URL(request.url);
if (route && url.pathname !== route) {
Expand Down Expand Up @@ -109,13 +112,16 @@ let didWarnAboutExperimentalCreateMcpHandler = false;
/**
* @deprecated This has been renamed to createMcpHandler, and experimental_createMcpHandler will be removed in the next major version
*/
export function experimental_createMcpHandler(
export function experimental_createMcpHandler<
Env = unknown,
Props = unknown
>(
server: McpServer | Server,
options: CreateMcpHandlerOptions = {}
): (
request: Request,
env: unknown,
ctx: ExecutionContext
env: Env,
ctx: ExecutionContext<Props>
) => Promise<Response> {
if (!didWarnAboutExperimentalCreateMcpHandler) {
didWarnAboutExperimentalCreateMcpHandler = true;
Expand Down
6 changes: 3 additions & 3 deletions packages/agents/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export abstract class McpAgent<
/** Return a handler for the given path for this MCP.
* Defaults to Streamable HTTP transport.
*/
static serve(
static serve<Env = unknown, Props = unknown>(
path: string,
{
binding = "MCP_OBJECT",
Expand All @@ -371,11 +371,11 @@ export abstract class McpAgent<
}: ServeOptions = {}
) {
return {
async fetch<Env>(
async fetch(
this: void,
request: Request,
env: Env,
ctx: ExecutionContext
ctx: ExecutionContext<Props>
): Promise<Response> {
// Handle CORS preflight
const corsResponse = handleCORS(request, corsOptions);
Expand Down
70 changes: 70 additions & 0 deletions packages/agents/src/tests-d/exported-handler-props.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Type tests for ExportedHandler with Props support
* @see https://github.com/cloudflare/agents/issues/501
*/
import type { ExportedHandler } from "../types";

type TestEnv = {
MY_VAR: string;
};

type TestProps = {
userId: string;
baseUrl: string;
};

// Props flows to fetch handler
{
const handler: ExportedHandler<TestEnv, TestProps> = {
async fetch(request, env, ctx) {
const _userId: string = ctx.props.userId;
const _myVar: string = env.MY_VAR;
return new Response("ok");
}
};
handler;
}

// Props flows to scheduled handler
{
const handler: ExportedHandler<TestEnv, TestProps> = {
async scheduled(controller, env, ctx) {
const _userId: string = ctx.props.userId;
}
};
handler;
}

// Props flows to queue handler
{
type QueueMessage = { data: string };
const handler: ExportedHandler<TestEnv, TestProps, QueueMessage> = {
async queue(batch, env, ctx) {
const _userId: string = ctx.props.userId;
const _data: string = batch.messages[0].body.data;
}
};
handler;
}

// satisfies pattern works
{
const handler = {
async fetch(request, env, ctx) {
const userId: string = ctx.props.userId;
return new Response(userId);
}
} satisfies ExportedHandler<TestEnv, TestProps>;
handler;
}

// Without Props, ctx.props is unknown (default)
{
const handler: ExportedHandler<TestEnv> = {
async fetch(request, env, ctx) {
const _props: unknown = ctx.props;
return new Response("ok");
}
};
handler;
}
119 changes: 119 additions & 0 deletions packages/agents/src/types.ts
Copy link
Contributor

@mattzcarey mattzcarey Jan 16, 2026

Choose a reason for hiding this comment

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

I'm not sure we should be typing these here. These are already defined in the output of wrangler types if I am not mistaken.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the quick reply! I want to make sure I understand the concern. Are you saying wrangler types already generates ExportedHandler with a Props parameter? I added Props support to enable typed ctx.props which I don't believe exists in the current wrangler types output. Should these types live elsewhere, or should I be extending the wrangler generated types differently?

Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,122 @@ export enum MessageType {
CF_AGENT_STATE = "cf_agent_state",
RPC = "rpc"
}

/**
* Fetch handler with Props support for ExecutionContext
*/
export type ExportedHandlerFetchHandler<
Env = unknown,
Props = unknown,
CfHostMetadata = unknown
> = (
request: Request<CfHostMetadata, IncomingRequestCfProperties<CfHostMetadata>>,
env: Env,
ctx: ExecutionContext<Props>
) => Response | Promise<Response>;

/**
* Tail handler with Props support for ExecutionContext
*/
export type ExportedHandlerTailHandler<Env = unknown, Props = unknown> = (
events: TraceItem[],
env: Env,
ctx: ExecutionContext<Props>
) => void | Promise<void>;

/**
* Trace handler with Props support for ExecutionContext
*/
export type ExportedHandlerTraceHandler<Env = unknown, Props = unknown> = (
traces: TraceItem[],
env: Env,
ctx: ExecutionContext<Props>
) => void | Promise<void>;

/**
* TailStream handler with Props support for ExecutionContext
*/
export type ExportedHandlerTailStreamHandler<Env = unknown, Props = unknown> = (
event: TailStream.TailEvent<TailStream.Onset>,
env: Env,
ctx: ExecutionContext<Props>
) => TailStream.TailEventHandlerType | Promise<TailStream.TailEventHandlerType>;

/**
* Scheduled handler with Props support for ExecutionContext
*/
export type ExportedHandlerScheduledHandler<Env = unknown, Props = unknown> = (
controller: ScheduledController,
env: Env,
ctx: ExecutionContext<Props>
) => void | Promise<void>;

/**
* Queue handler with Props support for ExecutionContext
*/
export type ExportedHandlerQueueHandler<
Env = unknown,
Props = unknown,
Message = unknown
> = (
batch: MessageBatch<Message>,
env: Env,
ctx: ExecutionContext<Props>
) => void | Promise<void>;

/**
* Test handler with Props support for ExecutionContext
*/
export type ExportedHandlerTestHandler<Env = unknown, Props = unknown> = (
controller: TestController,
env: Env,
ctx: ExecutionContext<Props>
) => void | Promise<void>;

/**
* Enhanced ExportedHandler interface that supports Props flowing to ExecutionContext.
*
* This interface extends the base @cloudflare/workers-types ExportedHandler
* to add a Props type parameter that flows through to ExecutionContext<Props>
* in all handler methods.
*
* @typeParam Env - The environment bindings type
* @typeParam Props - Props type that flows to ExecutionContext<Props>, making ctx.props typed
* @typeParam QueueHandlerMessage - Message type for queue handlers
* @typeParam CfHostMetadata - CF metadata type for fetch handlers
*
* @example
* ```typescript
* import type { ExportedHandler } from "agents/types";
*
* type Env = { DB: D1Database };
* type Props = { userId: string };
*
* export default {
* async fetch(request, env, ctx) {
* // ctx.props is now typed as Props
* const userId = ctx.props.userId;
* return new Response(`Hello ${userId}`);
* },
* async scheduled(controller, env, ctx) {
* // ctx.props is also typed here
* console.log(ctx.props.userId);
* }
* } satisfies ExportedHandler<Env, Props>;
* ```
*/
export interface ExportedHandler<
Env = unknown,
Props = unknown,
QueueHandlerMessage = unknown,
CfHostMetadata = unknown
> {
fetch?: ExportedHandlerFetchHandler<Env, Props, CfHostMetadata>;
tail?: ExportedHandlerTailHandler<Env, Props>;
trace?: ExportedHandlerTraceHandler<Env, Props>;
tailStream?: ExportedHandlerTailStreamHandler<Env, Props>;
scheduled?: ExportedHandlerScheduledHandler<Env, Props>;
test?: ExportedHandlerTestHandler<Env, Props>;
email?: EmailExportedHandler<Env>;
queue?: ExportedHandlerQueueHandler<Env, Props, QueueHandlerMessage>;
}
Loading