Skip to content
Merged
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/fuzzy-states-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

Add `createOAuthProvider` method to the `Agent` class, allowing subclasses to override the default OAuth provider used when connecting to MCP servers. This enables custom authentication strategies such as pre-registered client credentials or mTLS, beyond the built-in dynamic client registration.
17 changes: 17 additions & 0 deletions docs/mcp-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ this.mcp.configureOAuthCallback({
});
```

### Custom OAuth Provider

By default, agents use dynamic client registration to authenticate with MCP servers. If you need to use a different OAuth strategy — such as pre-registered client credentials, mTLS-based authentication, or other mechanisms — override the `createOAuthProvider` method in your agent subclass:

```typescript
import { Agent } from "agents";
import type { AgentsOAuthProvider } from "agents";

class MyAgent extends Agent {
createOAuthProvider(callbackUrl: string): AgentsOAuthProvider {
return new MyCustomOAuthProvider(this.ctx.storage, this.name, callbackUrl);
}
}
```

Your custom class must implement the `AgentsOAuthProvider` interface, which extends the MCP SDK's `OAuthClientProvider` with additional properties (`authUrl`, `clientId`, `serverId`) and methods (`checkState`, `consumeState`, `deleteCodeVerifier`) used by the agent's MCP connection lifecycle.

## Using MCP Capabilities

Once connected, access the server's capabilities:
Expand Down
20 changes: 4 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"esbuild": "^0.25.1"
},
"lint-staged": {
"*": [
"*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte,css}": [
"oxfmt --write"
]
},
Expand Down
41 changes: 35 additions & 6 deletions packages/agents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import type {
WorkflowPage
} from "./workflow-types";
import { MCPConnectionState } from "./mcp/client-connection";
import { DurableObjectOAuthClientProvider } from "./mcp/do-oauth-client-provider";
import {
DurableObjectOAuthClientProvider,
type AgentsOAuthProvider
} from "./mcp/do-oauth-client-provider";
import type { TransportType } from "./mcp/types";
import { genericObservability, type Observability } from "./observability";
import { DisposableStore } from "./core/events";
Expand Down Expand Up @@ -3406,11 +3409,7 @@ export class Agent<

const id = nanoid(8);

const authProvider = new DurableObjectOAuthClientProvider(
this.ctx.storage,
this.name,
callbackUrl
);
const authProvider = this.createOAuthProvider(callbackUrl);
authProvider.serverId = id;

// Use the transport type specified in options, or default to "auto"
Expand Down Expand Up @@ -3514,6 +3513,36 @@ export class Agent<
return mcpState;
}

/**
* Create the OAuth provider used when connecting to MCP servers that require authentication.
*
* Override this method in a subclass to supply a custom OAuth provider implementation,
* for example to use pre-registered client credentials, mTLS-based authentication,
* or any other OAuth flow beyond dynamic client registration.
*
* @example
* // Custom OAuth provider
* class MyAgent extends Agent {
* createOAuthProvider(callbackUrl: string): AgentsOAuthProvider {
* return new MyCustomOAuthProvider(
* this.ctx.storage,
* this.name,
* callbackUrl
* );
* }
* }
*
* @param callbackUrl The OAuth callback URL for the authorization flow
* @returns An {@link AgentsOAuthProvider} instance used by {@link addMcpServer}
*/
createOAuthProvider(callbackUrl: string): AgentsOAuthProvider {
return new DurableObjectOAuthClientProvider(
this.ctx.storage,
this.name,
callbackUrl
);
}

private broadcastMcpServers() {
this.broadcast(
JSON.stringify({
Expand Down
2 changes: 1 addition & 1 deletion packages/agents/src/tests/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export {
export type { TestState } from "./state";
export { TestDestroyScheduleAgent, TestScheduleAgent } from "./schedule";
export { TestWorkflowAgent } from "./workflow";
export { TestOAuthAgent } from "./oauth";
export { TestOAuthAgent, TestCustomOAuthAgent } from "./oauth";
export { TestReadonlyAgent } from "./readonly";
export { TestCallableAgent, TestParentAgent, TestChildAgent } from "./callable";
export { TestRaceAgent } from "./race";
64 changes: 64 additions & 0 deletions packages/agents/src/tests/agents/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Agent } from "../../index.ts";
import { DurableObjectOAuthClientProvider } from "../../mcp/do-oauth-client-provider";
import type { AgentsOAuthProvider } from "../../mcp/do-oauth-client-provider";
import type { MCPClientConnection } from "../../mcp/client-connection";

// Test Agent for OAuth client side flows
Expand Down Expand Up @@ -211,4 +213,66 @@ export class TestOAuthAgent extends Agent<Record<string, unknown>> {
// @ts-expect-error - accessing private property for testing
this._mcpConnectionsInitialized = false;
}

testCreateOAuthProvider(callbackUrl: string): {
isDurableObjectProvider: boolean;
callbackUrl: string;
} {
const provider = this.createOAuthProvider(callbackUrl);
return {
isDurableObjectProvider:
provider instanceof DurableObjectOAuthClientProvider,
callbackUrl: String(provider.redirectUrl ?? "")
};
}
}

// Test Agent that overrides createOAuthProvider with a custom implementation
export class TestCustomOAuthAgent extends Agent<Record<string, unknown>> {
observability = undefined;

private _customProviderCallbackUrl: string | undefined;

createOAuthProvider(callbackUrl: string): AgentsOAuthProvider {
this._customProviderCallbackUrl = callbackUrl;
// Return a minimal mock that satisfies the interface
return {
authUrl: undefined,
clientId: "custom-client-id",
serverId: undefined,
redirectUrl: callbackUrl,
get clientMetadata() {
return { redirect_uris: [callbackUrl] };
},
get clientUri() {
return callbackUrl;
},
checkState: async () => ({ valid: false }),
consumeState: async () => {},
deleteCodeVerifier: async () => {},
clientInformation: async () => undefined,
saveClientInformation: async () => {},
tokens: async () => undefined,
saveTokens: async () => {},
state: async () => "mock-state",
redirectToAuthorization: async () => {},
invalidateCredentials: async () => {},
saveCodeVerifier: async () => {},
codeVerifier: async () => "mock-verifier"
} as AgentsOAuthProvider;
}

testCreateOAuthProvider(callbackUrl: string): {
isDurableObjectProvider: boolean;
clientId: string | undefined;
callbackUrl: string | undefined;
} {
const provider = this.createOAuthProvider(callbackUrl);
return {
isDurableObjectProvider:
provider instanceof DurableObjectOAuthClientProvider,
clientId: provider.clientId,
callbackUrl: this._customProviderCallbackUrl
};
}
}
38 changes: 38 additions & 0 deletions packages/agents/src/tests/mcp/create-oauth-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { env } from "cloudflare:test";
import { describe, expect, it } from "vitest";
import type { Env } from "../worker";

declare module "cloudflare:test" {
interface ProvidedEnv extends Env {}
}

describe("createOAuthProvider", () => {
it("should return a DurableObjectOAuthClientProvider by default", async () => {
const agentId = env.TestOAuthAgent.idFromName("test-default-provider");
const agentStub = env.TestOAuthAgent.get(agentId);

await agentStub.setName("default");

const result = await agentStub.testCreateOAuthProvider(
"http://example.com/callback"
);

expect(result.isDurableObjectProvider).toBe(true);
expect(result.callbackUrl).toBe("http://example.com/callback");
});

it("should use a custom provider when overridden in a subclass", async () => {
const agentId = env.TestCustomOAuthAgent.idFromName("test-custom-provider");
const agentStub = env.TestCustomOAuthAgent.get(agentId);

await agentStub.setName("default");

const result = await agentStub.testCreateOAuthProvider(
"http://example.com/custom-callback"
);

expect(result.isDurableObjectProvider).toBe(false);
expect(result.clientId).toBe("custom-client-id");
expect(result.callbackUrl).toBe("http://example.com/custom-callback");
});
});
3 changes: 3 additions & 0 deletions packages/agents/src/tests/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
TestScheduleAgent,
TestWorkflowAgent,
TestOAuthAgent,
TestCustomOAuthAgent,
TestReadonlyAgent,
TestCallableAgent,
TestParentAgent,
Expand All @@ -42,6 +43,7 @@ import type {
TestCaseSensitiveAgent,
TestUserNotificationAgent,
TestOAuthAgent,
TestCustomOAuthAgent,
TestMcpJurisdiction,
TestDestroyScheduleAgent,
TestReadonlyAgent,
Expand All @@ -64,6 +66,7 @@ export type Env = {
CaseSensitiveAgent: DurableObjectNamespace<TestCaseSensitiveAgent>;
UserNotificationAgent: DurableObjectNamespace<TestUserNotificationAgent>;
TestOAuthAgent: DurableObjectNamespace<TestOAuthAgent>;
TestCustomOAuthAgent: DurableObjectNamespace<TestCustomOAuthAgent>;
TEST_MCP_JURISDICTION: DurableObjectNamespace<TestMcpJurisdiction>;
TestDestroyScheduleAgent: DurableObjectNamespace<TestDestroyScheduleAgent>;
TestReadonlyAgent: DurableObjectNamespace<TestReadonlyAgent>;
Expand Down
5 changes: 5 additions & 0 deletions packages/agents/src/tests/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
"class_name": "TestOAuthAgent",
"name": "TestOAuthAgent"
},
{
"class_name": "TestCustomOAuthAgent",
"name": "TestCustomOAuthAgent"
},
{
"class_name": "TestMcpJurisdiction",
"name": "TEST_MCP_JURISDICTION"
Expand Down Expand Up @@ -111,6 +115,7 @@
"TestUserNotificationAgent",
"TestRaceAgent",
"TestOAuthAgent",
"TestCustomOAuthAgent",
"TestMcpJurisdiction",
"TestDestroyScheduleAgent",
"TestReadonlyAgent",
Expand Down
Loading