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
6 changes: 6 additions & 0 deletions .changeset/tiny-maps-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@openai/agents-core": patch
"@openai/agents-openai": patch
---

Fix #374 add connector support
59 changes: 59 additions & 0 deletions examples/connectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Agent, run, hostedMcpTool } from '@openai/agents';

async function main(verbose: boolean, stream: boolean): Promise<void> {
// 1. Visit https://developers.google.com/oauthplayground/
// 2. Input https://www.googleapis.com/auth/calendar.events as the required scope
// 3. Grab the acccess token starting with "ya29."
const authorization = process.env.GOOGLE_CALENDAR_AUTHORIZATION!;

const agent = new Agent({
name: 'My Calendar Assistant',
instructions:
'You are a helpful assistant that can help a user with their calendar.',
tools: [
hostedMcpTool({
serverLabel: 'google_calendar',
connectorId: 'connector_googlecalendar',
authorization,
requireApproval: 'never',
}),
],
});

const today = new Date().toISOString().split('T')[0];
const input = `What is my schedule for ${today}?`;
if (stream) {
const result = await run(agent, input, { stream: true });
for await (const event of result) {
if (
event.type === 'raw_model_stream_event' &&
event.data.type === 'model' &&
event.data.event.type !== 'response.mcp_call_arguments.delta' &&
event.data.event.type !== 'response.output_text.delta'
) {
console.log(`Got event of type ${JSON.stringify(event.data)}`);
}
}
for (const item of result.newItems) {
console.log(JSON.stringify(item, null, 2));
}
console.log(`Done streaming; final result: ${result.finalOutput}`);
} else {
const res = await run(agent, input);
if (verbose) {
for (const item of res.output) {
console.log(JSON.stringify(item, null, 2));
}
}
console.log(res.finalOutput);
}
}

const args = process.argv.slice(2);
const verbose = args.includes('--verbose');
const stream = args.includes('--stream');

main(verbose, stream).catch((err) => {
console.error(err);
process.exit(1);
});
12 changes: 12 additions & 0 deletions examples/connectors/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": true,
"name": "connectors",
"dependencies": {
"@openai/agents": "workspace:*",
"zod": "^3.25.40"
},
"scripts": {
"build-check": "tsc --noEmit",
"start": "tsx index.ts"
}
}
3 changes: 3 additions & 0 deletions examples/connectors/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.examples.json"
}
162 changes: 118 additions & 44 deletions packages/agents-core/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,52 +134,126 @@ export type HostedMCPTool<Context = UnknownContext> = HostedTool & {
*/
export function hostedMcpTool<Context = UnknownContext>(
options: {
serverLabel: string;
serverUrl: string;
allowedTools?: string[] | { toolNames?: string[] };
headers?: Record<string, string>;
} & (
| { requireApproval?: never }
| { requireApproval: 'never' }
| {
requireApproval:
| 'always'
| {
never?: { toolNames: string[] };
always?: { toolNames: string[] };
};
onApproval?: HostedMCPApprovalFunction<Context>;
}
),
): HostedMCPTool<Context> {
const providerData: ProviderData.HostedMCPTool<Context> =
typeof options.requireApproval === 'undefined' ||
options.requireApproval === 'never'
? {
type: 'mcp',
server_label: options.serverLabel,
server_url: options.serverUrl,
require_approval: 'never',
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
headers: options.headers,
} &
// MCP server
(| {
serverLabel: string;
serverUrl?: string;
authorization?: string;
headers?: Record<string, string>;
}
: {
type: 'mcp',
server_label: options.serverLabel,
server_url: options.serverUrl,
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
headers: options.headers,
require_approval:
typeof options.requireApproval === 'string'
? 'always'
: buildRequireApproval(options.requireApproval),
on_approval: options.onApproval,
};
return {
type: 'hosted_tool',
name: 'hosted_mcp',
providerData,
};
// OpenAI Connector
| {
serverLabel: string;
connectorId: string;
authorization?: string;
headers?: Record<string, string>;
}
) &
(
| { requireApproval?: never }
| { requireApproval: 'never' }
| {
requireApproval:
| 'always'
| {
never?: { toolNames: string[] };
always?: { toolNames: string[] };
};
onApproval?: HostedMCPApprovalFunction<Context>;
}
),
Comment on lines +140 to +166

Choose a reason for hiding this comment

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

[P1] Require either serverUrl or connectorId for hosted MCP tools

The new overload for hostedMcpTool makes serverUrl optional in both branches while connectorId is only required in the connector branch. Because the runtime dispatch simply checks 'serverUrl' in options and 'connectorId' in options, it is now possible to call hostedMcpTool({ serverLabel: 'foo' }) without either value; this compiles and returns provider data that lacks both server_url and connector_id. When the model later tries to invoke the tool, OpenAI will reject the call because no endpoint or connector is specified, whereas the previous signature enforced the required field at compile time. Consider keeping serverUrl mandatory for non‑connector usage and validating that one of the two identifiers is provided so misconfigurations are caught early.

Useful? React with 👍 / 👎.

): HostedMCPTool<Context> {
if ('serverUrl' in options) {
// the MCP servers comaptible with the specification
const providerData: ProviderData.HostedMCPTool<Context> =
typeof options.requireApproval === 'undefined' ||
options.requireApproval === 'never'
? {
type: 'mcp',
server_label: options.serverLabel,
server_url: options.serverUrl,
require_approval: 'never',
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
headers: options.headers,
}
: {
type: 'mcp',
server_label: options.serverLabel,
server_url: options.serverUrl,
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
headers: options.headers,
require_approval:
typeof options.requireApproval === 'string'
? 'always'
: buildRequireApproval(options.requireApproval),
on_approval: options.onApproval,
};
return {
type: 'hosted_tool',
name: 'hosted_mcp',
providerData,
};
} else if ('connectorId' in options) {
// OpenAI's connectors
const providerData: ProviderData.HostedMCPTool<Context> =
typeof options.requireApproval === 'undefined' ||
options.requireApproval === 'never'
? {
type: 'mcp',
server_label: options.serverLabel,
connector_id: options.connectorId,
authorization: options.authorization,
require_approval: 'never',
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
headers: options.headers,
}
: {
type: 'mcp',
server_label: options.serverLabel,
connector_id: options.connectorId,
authorization: options.authorization,
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
headers: options.headers,
require_approval:
typeof options.requireApproval === 'string'
? 'always'
: buildRequireApproval(options.requireApproval),
on_approval: options.onApproval,
};
return {
type: 'hosted_tool',
name: 'hosted_mcp',
providerData,
};
} else {
// the MCP servers comaptible with the specification
const providerData: ProviderData.HostedMCPTool<Context> =
typeof options.requireApproval === 'undefined' ||
options.requireApproval === 'never'
? {
type: 'mcp',
server_label: options.serverLabel,
require_approval: 'never',
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
}
: {
type: 'mcp',
server_label: options.serverLabel,
allowed_tools: toMcpAllowedToolsFilter(options.allowedTools),
require_approval:
typeof options.requireApproval === 'string'
? 'always'
: buildRequireApproval(options.requireApproval),
on_approval: options.onApproval,
};
return {
type: 'hosted_tool',
name: 'hosted_mcp',
providerData,
};
}
}

/**
Expand Down
44 changes: 29 additions & 15 deletions packages/agents-core/src/types/providerData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,35 @@ import { UnknownContext } from './aliases';
*/
export type HostedMCPTool<Context = UnknownContext> = {
type: 'mcp';
server_label: string;
server_url: string;
allowed_tools?: string[] | { tool_names: string[] };
headers?: Record<string, string>;
} & (
| { require_approval?: 'never'; on_approval?: never }
| {
require_approval:
| 'always'
| {
never?: { tool_names: string[] };
always?: { tool_names: string[] };
};
on_approval?: HostedMCPApprovalFunction<Context>;
}
);
} &
// MCP server
(| {
server_label: string;
server_url?: string;
authorization?: string;
headers?: Record<string, string>;
}
// OpenAI Connector
| {
server_label: string;
connector_id: string;
authorization?: string;
headers?: Record<string, string>;
}
) &
(
| { require_approval?: 'never'; on_approval?: never }
| {
require_approval:
| 'always'
| {
never?: { tool_names: string[] };
always?: { tool_names: string[] };
};
on_approval?: HostedMCPApprovalFunction<Context>;
}
);

export type HostedMCPListTools = {
id: string;
Expand All @@ -39,6 +52,7 @@ export type HostedMCPCall = {
arguments: string;
name: string;
server_label: string;
connector_id?: string;
error?: string | null;
// excluding this large data field
// output?: string | null;
Expand Down
2 changes: 2 additions & 0 deletions packages/agents-openai/src/openaiResponsesModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ function converTool<_TContext = unknown>(
type: 'mcp',
server_label: tool.providerData.server_label,
server_url: tool.providerData.server_url,
connector_id: tool.providerData.connector_id,
authorization: tool.providerData.authorization,
allowed_tools: tool.providerData.allowed_tools,
headers: tool.providerData.headers,
require_approval: convertMCPRequireApproval(
Expand Down
16 changes: 10 additions & 6 deletions pnpm-lock.yaml

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