Skip to content

Commit a2f34be

Browse files
feat(openai): support for MCP connector tool
1 parent 79c52aa commit a2f34be

File tree

7 files changed

+525
-6
lines changed

7 files changed

+525
-6
lines changed

libs/providers/langchain-openai/src/chat_models/completions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
type BaseMessage,
77
isAIMessage,
88
type UsageMetadata,
9-
type BaseMessageFields,
9+
type AIMessageFields,
1010
BaseMessageChunk,
1111
} from "@langchain/core/messages";
1212
import {
@@ -273,7 +273,7 @@ export class ChatOpenAICompletions<
273273
Object.entries(generation.message).filter(
274274
([key]) => !key.startsWith("lc_")
275275
)
276-
) as BaseMessageFields
276+
) as AIMessageFields
277277
);
278278
generations.push(generation);
279279
}

libs/providers/langchain-openai/src/converters/responses.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,13 +1116,13 @@ export const convertMessagesToResponsesInput: Converter<
11161116
}
11171117

11181118
// ai content
1119-
let { content } = lcMsg;
1119+
let { content } = lcMsg as { content: ContentBlock[] };
11201120
if (additional_kwargs?.refusal) {
11211121
if (typeof content === "string") {
11221122
content = [{ type: "output_text", text: content, annotations: [] }];
11231123
}
11241124
content = [
1125-
...content,
1125+
...(content as ContentBlock[]),
11261126
{ type: "refusal", refusal: additional_kwargs.refusal },
11271127
];
11281128
}
@@ -1224,7 +1224,7 @@ export const convertMessagesToResponsesInput: Converter<
12241224
}
12251225

12261226
const messages: ResponsesInputItem[] = [];
1227-
const content = lcMsg.content.flatMap((item) => {
1227+
const content = (lcMsg.content as ContentBlock[]).flatMap((item) => {
12281228
if (item.type === "mcp_approval_response") {
12291229
messages.push({
12301230
type: "mcp_approval_response",

libs/providers/langchain-openai/src/tools/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ export type {
77
WebSearchOptions,
88
} from "./webSearch.js";
99

10+
import { mcp } from "./mcp.js";
11+
export type {
12+
McpTool,
13+
McpConnectorId,
14+
McpToolFilter,
15+
McpApprovalFilter,
16+
McpRemoteServerOptions,
17+
McpConnectorOptions,
18+
} from "./mcp.js";
19+
1020
export const tools = {
1121
webSearch,
22+
mcp,
1223
};
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { OpenAI as OpenAIClient } from "openai";
2+
3+
/**
4+
* Available connector IDs for OpenAI's built-in service connectors.
5+
* These are OpenAI-maintained MCP wrappers for popular services.
6+
*/
7+
export type McpConnectorId =
8+
| "connector_dropbox"
9+
| "connector_gmail"
10+
| "connector_googlecalendar"
11+
| "connector_googledrive"
12+
| "connector_microsoftteams"
13+
| "connector_outlookcalendar"
14+
| "connector_outlookemail"
15+
| "connector_sharepoint";
16+
17+
/**
18+
* Filter object to specify which tools are allowed.
19+
*/
20+
export interface McpToolFilter {
21+
/**
22+
* List of allowed tool names.
23+
*/
24+
toolNames?: string[];
25+
/**
26+
* Indicates whether or not a tool modifies data or is read-only.
27+
* If an MCP server is annotated with `readOnlyHint`, it will match this filter.
28+
*/
29+
readOnly?: boolean;
30+
}
31+
32+
/**
33+
* Filter object for approval requirements.
34+
*/
35+
export interface McpApprovalFilter {
36+
/**
37+
* Tools that always require approval before execution.
38+
*/
39+
always?: McpToolFilter;
40+
/**
41+
* Tools that never require approval.
42+
*/
43+
never?: McpToolFilter;
44+
}
45+
46+
/**
47+
* Base options shared between remote MCP servers and connectors.
48+
*/
49+
interface McpBaseOptions {
50+
/**
51+
* A label for this MCP server, used to identify it in tool calls.
52+
*/
53+
serverLabel: string;
54+
/**
55+
* List of allowed tool names or a filter object.
56+
* Use this to limit which tools from the MCP server are available to the model.
57+
*/
58+
allowedTools?: string[] | McpToolFilter;
59+
/**
60+
* An OAuth access token for authentication with the MCP server.
61+
* Your application must handle the OAuth authorization flow and provide the token here.
62+
*/
63+
authorization?: string;
64+
/**
65+
* Optional HTTP headers to send to the MCP server.
66+
* Use for authentication or other purposes.
67+
*/
68+
headers?: Record<string, string>;
69+
/**
70+
* Specify which of the MCP server's tools require approval before execution.
71+
* - `"always"`: All tools require approval
72+
* - `"never"`: No tools require approval
73+
* - `McpApprovalFilter`: Fine-grained control over which tools require approval
74+
*
75+
* @default "always" (approval required for all tools)
76+
*/
77+
requireApproval?: "always" | "never" | McpApprovalFilter;
78+
/**
79+
* Optional description of the MCP server, used to provide more context to the model.
80+
*/
81+
serverDescription?: string;
82+
}
83+
84+
/**
85+
* Options for connecting to a remote MCP server via URL.
86+
*/
87+
export interface McpRemoteServerOptions extends McpBaseOptions {
88+
/**
89+
* The URL for the MCP server.
90+
* The server must implement the Streamable HTTP or HTTP/SSE transport protocol.
91+
*/
92+
serverUrl: string;
93+
}
94+
95+
/**
96+
* Options for connecting to an OpenAI-maintained service connector.
97+
*/
98+
export interface McpConnectorOptions extends McpBaseOptions {
99+
/**
100+
* Identifier for the service connector.
101+
* These are OpenAI-maintained MCP wrappers for popular services.
102+
*
103+
* Available connectors:
104+
* - `connector_dropbox`: Dropbox file access
105+
* - `connector_gmail`: Gmail email access
106+
* - `connector_googlecalendar`: Google Calendar access
107+
* - `connector_googledrive`: Google Drive file access
108+
* - `connector_microsoftteams`: Microsoft Teams access
109+
* - `connector_outlookcalendar`: Outlook Calendar access
110+
* - `connector_outlookemail`: Outlook Email access
111+
* - `connector_sharepoint`: SharePoint file access
112+
*/
113+
connectorId: McpConnectorId;
114+
}
115+
116+
/**
117+
* OpenAI MCP tool type for the Responses API.
118+
*/
119+
export type McpTool = OpenAIClient.Responses.Tool.Mcp;
120+
121+
/**
122+
* Converts a McpToolFilter to the API format.
123+
*/
124+
function convertToolFilter(
125+
filter: McpToolFilter
126+
): OpenAIClient.Responses.Tool.Mcp.McpToolFilter {
127+
return {
128+
tool_names: filter.toolNames,
129+
read_only: filter.readOnly,
130+
};
131+
}
132+
133+
/**
134+
* Converts allowed_tools option to API format.
135+
*/
136+
function convertAllowedTools(
137+
allowedTools: string[] | McpToolFilter | undefined
138+
): Array<string> | OpenAIClient.Responses.Tool.Mcp.McpToolFilter | undefined {
139+
if (!allowedTools) return undefined;
140+
if (Array.isArray(allowedTools)) return allowedTools;
141+
return convertToolFilter(allowedTools);
142+
}
143+
144+
/**
145+
* Converts require_approval option to API format.
146+
*/
147+
function convertRequireApproval(
148+
requireApproval: "always" | "never" | McpApprovalFilter | undefined
149+
):
150+
| OpenAIClient.Responses.Tool.Mcp.McpToolApprovalFilter
151+
| "always"
152+
| "never"
153+
| undefined {
154+
if (!requireApproval) return undefined;
155+
if (typeof requireApproval === "string") return requireApproval;
156+
return {
157+
always: requireApproval.always
158+
? convertToolFilter(requireApproval.always)
159+
: undefined,
160+
never: requireApproval.never
161+
? convertToolFilter(requireApproval.never)
162+
: undefined,
163+
};
164+
}
165+
166+
/**
167+
* Creates an MCP tool that connects to a remote MCP server or OpenAI service connector.
168+
* This allows OpenAI models to access external tools and services via the Model Context Protocol.
169+
*
170+
* There are two ways to use MCP tools:
171+
* 1. **Remote MCP servers**: Connect to any server on the public Internet that implements
172+
* the MCP protocol using `serverUrl`.
173+
* 2. **Connectors**: Use OpenAI-maintained MCP wrappers for popular services like
174+
* Google Workspace or Dropbox using `connectorId`.
175+
*
176+
* @see {@link https://platform.openai.com/docs/guides/tools-remote-mcp | OpenAI MCP Documentation}
177+
*
178+
* @param options - Configuration options for the MCP tool
179+
* @returns An MCP tool definition to be passed to the OpenAI Responses API
180+
*
181+
* @example
182+
* ```typescript
183+
* import { ChatOpenAI, tools } from "@langchain/openai";
184+
*
185+
* const model = new ChatOpenAI({ model: "gpt-4o" });
186+
*
187+
* // Using a remote MCP server
188+
* const response = await model.invoke("Roll 2d4+1", {
189+
* tools: [tools.mcp({
190+
* serverLabel: "dmcp",
191+
* serverDescription: "A D&D MCP server for dice rolling",
192+
* serverUrl: "https://dmcp-server.deno.dev/sse",
193+
* requireApproval: "never",
194+
* })],
195+
* });
196+
*
197+
* // Using a connector (e.g., Google Calendar)
198+
* const calendarResponse = await model.invoke("What's on my calendar today?", {
199+
* tools: [tools.mcp({
200+
* serverLabel: "google_calendar",
201+
* connectorId: "connector_googlecalendar",
202+
* authorization: "<oauth-access-token>",
203+
* requireApproval: "never",
204+
* })],
205+
* });
206+
*
207+
* // With tool filtering - only allow specific tools
208+
* const filteredResponse = await model.invoke("Roll some dice", {
209+
* tools: [tools.mcp({
210+
* serverLabel: "dmcp",
211+
* serverUrl: "https://dmcp-server.deno.dev/sse",
212+
* allowedTools: ["roll"], // Only allow the "roll" tool
213+
* requireApproval: "never",
214+
* })],
215+
* });
216+
*
217+
* // With fine-grained approval control
218+
* const controlledResponse = await model.invoke("Search and modify files", {
219+
* tools: [tools.mcp({
220+
* serverLabel: "deepwiki",
221+
* serverUrl: "https://mcp.deepwiki.com/mcp",
222+
* requireApproval: {
223+
* never: { toolNames: ["ask_question", "read_wiki_structure"] },
224+
* // All other tools will require approval
225+
* },
226+
* })],
227+
* });
228+
* ```
229+
*/
230+
export function mcp(options: McpRemoteServerOptions): McpTool;
231+
export function mcp(options: McpConnectorOptions): McpTool;
232+
export function mcp(
233+
options: McpRemoteServerOptions | McpConnectorOptions
234+
): McpTool {
235+
const baseConfig: McpTool = {
236+
type: "mcp",
237+
server_label: options.serverLabel,
238+
allowed_tools: convertAllowedTools(options.allowedTools),
239+
authorization: options.authorization,
240+
headers: options.headers,
241+
require_approval: convertRequireApproval(options.requireApproval),
242+
server_description: options.serverDescription,
243+
};
244+
245+
if ("serverUrl" in options) {
246+
return {
247+
...baseConfig,
248+
server_url: options.serverUrl,
249+
};
250+
}
251+
252+
return {
253+
...baseConfig,
254+
connector_id: options.connectorId,
255+
};
256+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { expect, it, describe } from "vitest";
2+
import {
3+
HumanMessage,
4+
AIMessage,
5+
ContentBlock,
6+
} from "@langchain/core/messages";
7+
8+
import { tools } from "../index.js";
9+
import { ChatOpenAI } from "../../chat_models/index.js";
10+
11+
describe("OpenAI MCP Tool Integration Tests", () => {
12+
it("mcp connects to a remote MCP server and executes a tool", async () => {
13+
const llm = new ChatOpenAI({ model: "gpt-4o-mini" });
14+
const llmWithMcp = llm.bindTools([
15+
tools.mcp({
16+
serverLabel: "dmcp",
17+
serverDescription:
18+
"A Dungeons and Dragons MCP server to assist with dice rolling.",
19+
serverUrl: "https://dmcp-server.deno.dev/sse",
20+
requireApproval: "never",
21+
}),
22+
]);
23+
24+
const response = await llmWithMcp.invoke([
25+
new HumanMessage("Roll 2d6+3 for me"),
26+
]);
27+
28+
expect(response).toBeInstanceOf(AIMessage);
29+
expect(Array.isArray(response.content)).toBe(true);
30+
expect((response.content[0] as ContentBlock.Text).text).toContain(
31+
"You rolled a total of"
32+
);
33+
});
34+
});

0 commit comments

Comments
 (0)