-
Notifications
You must be signed in to change notification settings - Fork 836
Add support for Connector ID and other follow ups #6881
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
{ | ||
ServerName = Throw.IfNullOrWhitespace(serverName); | ||
ConnectorID = Throw.IfNullOrWhitespace(connectorID); | ||
Url = url; // needed for disambiguation. |
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.
Connector ID seems very specific to OpenAI?
With their connectors, do you specify both a name and a URL? I thought they'd be mutually exclusive, in which case I was expecting we'd just have a single "location" (pick a better word) that's either a url or an identifier. That also means it doesn't make it specific to OpenAI.
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.
I thought they'd be mutually exclusive,
They are mutually exclusive. This argument is just here for disambiguation because there's already a .ctor(string, string)
.
we'd just have a single "location" (pick a better word) that's either a url or an identifier
Do you mean having a type like struct ServerLocation
with mutually exclusive properties Url and ConnectorID(for the lack of a better name)? If so, that's sounds fine too, just wanted to make sure that we don't want to follow the simple route of having a factory method with a string connectorId
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.
I meant just having it be a string, and having the implementation decide whether it's a name or a url, e.g. does it start with a schema. Would that work? Is it problematic?
They are mutually exclusive. This argument is just here for disambiguation because there's already a .ctor(string, string).
We should not do that. Having a constructor that accepts both makes it look like both can be provided and utilized.
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.
I meant just having it be a string, and having the implementation decide whether it's a name or a url
Ah so you mean updating the existing .ctor(string, string url) and rename the param to urlOrConnectorId and then have something like
if (Uri.TryCreate(urlOrConnectorId, UriKind.Absolute, out url))
Url = url
else
ConnectorID = urlOrConnectorId
My only concern with that is that we will be overloading the argument.
We should not do that. Having a constructor that accepts both makes it look like both can be provided and utilized.
I thought it was fine since it was private, but OK, will take note.
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.
Ah so you mean updating the existing .ctor(string, string url) and rename the param
Yes, with some name that conveys "this is the thing to connect to, which could be a url or could be a descriptor that the backend service understands"
I thought it was fine since it was private
Ah, I didn't see that it was private.
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.
Do other services that support remote MCP, like Anthropic, have such a concept yet?
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.
like Anthropic, have such a concept yet?
No, I know anthropic doesn't from looking at their docs. I'm not aware of other providers that support remote MCP.
src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs
Outdated
Show resolved
Hide resolved
* Add AuthorizationToken property since it is now promoted in both OpenAI and Anthropic * Relax McpServerToolCallContent ToolName and ServerName
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.
Pull Request Overview
This PR adds support for connector ID in MCP (Model Context Protocol) tools and implements several related improvements. The changes enable the use of connector identifiers instead of just URLs for MCP servers, add OAuth authorization support, and relax validation requirements for tool properties.
- Add connector ID support to
HostedMcpServerTool
allowing string addresses that can be either URLs or connector IDs - Add
AuthorizationToken
property toHostedMcpServerTool
for OAuth authentication with MCP servers - Relax validation in
McpServerToolCallContent
by makingToolName
andServerName
optional properties - Enable user role messages to create approval responses for MCP tool calls
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs |
Updated constructor to accept string addresses, added AuthorizationToken property, simplified API |
src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs |
Made ToolName and ServerName optional settable properties, simplified constructor validation |
src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs |
Updated to handle connector IDs vs URLs, support authorization tokens, and allow user role approval responses |
test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs |
Updated tests to reflect new constructor signature and added AuthorizationToken property tests |
test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs |
Updated tests for simplified constructor and optional properties |
test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs |
Added comprehensive test for MCP tool approval workflow with both user and tool roles |
test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs |
Added integration test for connector-based MCP servers with authorization |
/// Gets or sets the name of the tool called. | ||
/// </summary> | ||
public string ToolName { get; } | ||
public string? ToolName { get; set; } |
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.
I'm still not convinced this makes sense. A call without any information about what's being called is useless. ToolName is what's being called. In contrast, ServerName is valuable as a disambiguator, but in general it's just informative.
If you really believe it's all or nothing, that both ToolName and ServerName need to be required or neither need to be required, then we should just leave it as is rather than have neither be required.
/// <exception cref="ArgumentNullException"><paramref name="serverName"/> or <paramref name="url"/> is <see langword="null"/>.</exception> | ||
/// <exception cref="ArgumentException"><paramref name="serverName"/> is empty or composed entirely of whitespace.</exception> | ||
public HostedMcpServerTool(string serverName, Uri url) | ||
/// <param name="serverAddress">The address of the remote MCP server.</param> |
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.
/// <param name="serverAddress">The address of the remote MCP server.</param> | |
/// <param name="serverAddress">The address of the remote MCP server. This may be a URL, or in the case of a service providing built-in MCP servers with known names, it can be such a name.</param> |
/// Gets or sets the OAuth authorization token that the AI service should use when calling the remote MCP server. | ||
/// </summary> | ||
public Uri Url { get; } | ||
public string? AuthorizationToken { get; set; } |
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.
What is the relationship between this and Headers?
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.
I expect that this should serve the same purpose as Headers.Add("Authorization", "<token>")
. However, I tested with the connector and endpoint returns Cannot specify 'headers' parameter with 'connector_id'.
. A bit unfortunate that OpenAI's ResponseTool.CreateMcpTool
accepts them.
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.
We need to choose the right shared representation, and then each implementation maps that apppropriately. I don't understand the argument for having both, if auth is representable as a header. You couldn't extract the auth token from the headers collection of just that collection existed, removing it from the collection passed to the underlying impl?
Is the idea that you always use this property for an auth token and the headers collection for everything else?
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.
Is the idea that you always use this property for an auth token and the headers collection for everything else?
Yes. OpenAI started with only Headers and now it promoted the auth header to be its own field in the payload.
I think 99% use cases will be for the authorization header. Having only Headers is more future proof but is less straightforward. Anthropic also only accepts the auth_token. It sucks to not have a crystal ball.
We can switch to only have AuthorizationToken. If someone wants to pass headers in OpenAI they can pass the raw tool. And if Headers becomes a thing in the future because more providers start adding them, we can bring it back. Wdyt?
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.
We can switch to only have AuthorizationToken. [...] And if Headers becomes a thing in the future because more providers start adding them, we can bring it back. Wdyt?
Do we have any examples where headers are required? @halter73, any opinions?
Otherwise, sounds good.
{ | ||
yield return ResponseItem.CreateMcpApprovalResponseItem(mcpApprovalResponseContent.Id, mcpApprovalResponseContent.Approved); | ||
} | ||
} |
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.
Were MCP approvals just broken before?
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, I see, this is being done to accomodate the approval being in a user message in addition to being in a tools message, where it's currently handled?
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.
For ChatRole.User, yes. This change is in response to your point in #6779.
The OpenAI Responses implementation currently looks for the tool approval as part of a Tool message. Is that right? Should it be User instead? Should it be any kind of message?
After looking into what kind of messages the other roles process, I think it doesn't make sense to handle mcp approvals for system/developer/assistant. However, worth mentioning that I think Function Approvals are handled for any role.
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.
to accomodate the approval being in a user message in addition to being in a tools message, where it's currently handled?
Yes.
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.
However, worth mentioning that I think Function Approvals are handled for any role.
Where? I see handling for it under case ChatRole.Tool
.
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.
In FunctionInvokingChatClient, I don't think there's anything preventing you from passing the FunctionApprovalResponseContent in a message with any role, not just user/tool.
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.
Ah, I see. @westey-m, is that desirable?
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.
I typically prefer to be stricter, since it helps to catch bugs. E.g. passing FARC in anything but a user message sounds like a bug to me. It is easy to relax the restriction later if we find a real scenario for it.
So I think tightening this in FICC as well makes sense.
{ | ||
yield return ResponseItem.CreateUserMessageItem(ToResponseContentParts(input.Contents)); | ||
|
||
foreach (AIContent item in input.Contents) |
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.
If the user message exists just to carry MCP approval, won't the above end up producing a separate empty user message item (with a dummy content part)?
Seems like it'd be better to restructure this, where this becomes:
yield return CreateUserResponseItems()
(or just do it inline as it's done for other kinds), iterating over the contents once rather than twice, and yielding the appropriate response items.
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.
Yes, it does create that dummy message you describe, I left it like that because that's the case for any other kind of content that doesn't map to a ResponseContentPart
but it does seem like something that should be fixed. Will do.
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.
that's the case for any other kind of content that doesn't map to a ResponseContentPart
Not exactly. Someone would need to create an empty ChatMessage to hit that. With this, they've created a ChatMessage that contains just the McpApproval, which would be a very common thing to do (just as it is for creating tool messages just with function approval), yet we're then treating it as an empty message.
Contributes to #6492.