Skip to content

Conversation

jozkee
Copy link
Member

@jozkee jozkee commented Oct 2, 2025

Contributes to #6492.

  • Add support for connector ID.
  • Add AuthorizationToken to HostedMcpServerTool since its now promoted in both OpenAI and Anthropic.
  • Relax McpServerToolCallContent ToolName and ServerName
  • Messages with user role can also create approval responses.

@github-actions github-actions bot added the area-ai Microsoft.Extensions.AI libraries label Oct 2, 2025
{
ServerName = Throw.IfNullOrWhitespace(serverName);
ConnectorID = Throw.IfNullOrWhitespace(connectorID);
Url = url; // needed for disambiguation.
Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member Author

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.

jozkee added 3 commits October 8, 2025 11:50
* Add AuthorizationToken property since it is now promoted in both OpenAI and Anthropic
* Relax McpServerToolCallContent ToolName and ServerName
@jozkee jozkee changed the title Add support for Connector ID Add support for Connector ID and other follow ups Oct 8, 2025
@jozkee jozkee marked this pull request as ready for review October 8, 2025 23:20
@jozkee jozkee requested a review from a team as a code owner October 8, 2025 23:20
@Copilot Copilot AI review requested due to automatic review settings October 8, 2025 23:20
Copy link
Contributor

@Copilot Copilot AI left a 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 to HostedMcpServerTool for OAuth authentication with MCP servers
  • Relax validation in McpServerToolCallContent by making ToolName and ServerName 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; }
Copy link
Member

@stephentoub stephentoub Oct 9, 2025

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>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// <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; }
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

@stephentoub stephentoub Oct 9, 2025

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?

Copy link
Member Author

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?

Copy link
Member

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);
}
}
Copy link
Member

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?

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Contributor

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)
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-ai Microsoft.Extensions.AI libraries

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants