88using System . Reflection ;
99using System . Text ;
1010using System . Text . Json ;
11+ using System . Threading . Tasks ;
1112using OpenAI . Chat ;
1213
1314namespace Azure . Projects . OpenAI ;
@@ -18,8 +19,12 @@ public class ChatTools
1819 private static readonly BinaryData s_noparams = BinaryData . FromString ( """{ "type" : "object", "properties" : {} }""" ) ;
1920
2021 private readonly Dictionary < string , MethodInfo > _methods = [ ] ;
22+ private readonly Dictionary < string , Func < string , BinaryData , Task < BinaryData > > > _mcpMethods = [ ] ;
2123 private readonly List < ChatTool > _definitions = [ ] ;
2224
25+ private List < McpClient > _mcpClients = [ ] ;
26+ private Dictionary < string , McpClient > _mcpClientsByEndpoint = [ ] ;
27+
2328 /// <summary>
2429 /// Initializes a new instance of the <see cref="ChatTools"/> class.
2530 /// </summary>
@@ -30,6 +35,20 @@ public ChatTools(params Type[] tools)
3035 Add ( functionHolder ) ;
3136 }
3237
38+ /// <summary>
39+ /// Adds a new MCP Server connection to be used for function calls.
40+ /// </summary>
41+ /// <param name="serverEndpoint">The Uri of the MCP Server.</param>
42+ public async Task AddMcpServerAsync ( Uri serverEndpoint )
43+ {
44+ var client = new McpClient ( serverEndpoint ) ;
45+ _mcpClientsByEndpoint [ serverEndpoint . AbsoluteUri ] = client ;
46+ await client . StartAsync ( ) . ConfigureAwait ( false ) ;
47+ BinaryData tools = await client . ListToolsAsync ( ) . ConfigureAwait ( false ) ;
48+ Add ( tools , client ) ;
49+ _mcpClients . Add ( client ) ;
50+ }
51+
3352 /// <summary>
3453 /// Gets the tool definitions.
3554 /// </summary>
@@ -49,6 +68,42 @@ public static implicit operator ChatCompletionOptions(ChatTools tools)
4968 return options ;
5069 }
5170
71+ /// <summary>
72+ /// Adds tool definitions from a JSON array in BinaryData format.
73+ /// </summary>
74+ /// <param name="toolDefinitions">BinaryData containing a JSON array of tool definitions</param>
75+ /// <param name="client">The McpClient.</param>
76+ /// <exception cref="ArgumentNullException">Thrown when toolDefinitions is null</exception>
77+ /// <exception cref="JsonException">Thrown when JSON parsing fails</exception>
78+ internal void Add ( BinaryData toolDefinitions , McpClient client )
79+ {
80+ using var document = JsonDocument . Parse ( toolDefinitions ) ;
81+ if ( ! document . RootElement . TryGetProperty ( "tools" , out JsonElement toolsElement ) )
82+ {
83+ throw new JsonException ( "The JSON document must contain a 'tools' array." ) ;
84+ }
85+
86+ var tools = toolsElement . EnumerateArray ( ) ;
87+ // the replacement is to deal with OpenAI's tool name regex validation.
88+ var serverKey = client . ServerEndpoint . AbsoluteUri . Replace ( '/' , '_' ) . Replace ( ':' , '_' ) ;
89+
90+ foreach ( var tool in tools )
91+ {
92+ var name = $ "{ serverKey } __.__{ tool . GetProperty ( "name" ) . GetString ( ) ! } ";
93+ var description = tool . GetProperty ( "description" ) . GetString ( ) ! ;
94+ var inputSchema = tool . GetProperty ( "inputSchema" ) . GetRawText ( ) ;
95+
96+ var chatTool = ChatTool . CreateFunctionTool (
97+ name ,
98+ description ,
99+ BinaryData . FromString ( inputSchema ) ) ;
100+
101+ _definitions . Add ( chatTool ) ;
102+
103+ _mcpMethods [ name ] = client . CallToolAsync ;
104+ }
105+ }
106+
52107 /// <summary>
53108 /// Adds a set of functions to the chat functions.
54109 /// </summary>
@@ -128,6 +183,26 @@ public string Call(ChatToolCall call)
128183 return result ;
129184 }
130185
186+ private async Task < string > CallMcp ( ChatToolCall call )
187+ {
188+ if ( _mcpMethods . TryGetValue ( call . FunctionName , out Func < string , BinaryData , Task < BinaryData > > ? method ) )
189+ {
190+ #if ! NETSTANDARD2_0
191+ var actualFunctionName = call . FunctionName . Split ( "__.__" , 2 ) [ 1 ] ;
192+ #else
193+ var separator = "__.__" ;
194+ var index = call . FunctionName . IndexOf ( separator ) ;
195+ var actualFunctionName = call . FunctionName . Substring ( index + separator . Length ) ;
196+ #endif
197+ var result = await method ( actualFunctionName , call . FunctionArguments ) . ConfigureAwait ( false ) ;
198+ return result . ToString ( ) ;
199+ }
200+ else
201+ {
202+ throw new NotImplementedException ( $ "MCP tool { call . FunctionName } not found.") ;
203+ }
204+ }
205+
131206 /// <summary>
132207 /// Calls all the specified <see cref="ChatToolCall"/>s.
133208 /// </summary>
@@ -148,24 +223,32 @@ public IEnumerable<ToolChatMessage> CallAll(IEnumerable<ChatToolCall> toolCalls)
148223 /// Calls all the specified <see cref="ChatToolCall"/>s.
149224 /// </summary>
150225 /// <param name="toolCalls"></param>
151- /// <param name="failed"></param>
152226 /// <returns></returns>
153- public IEnumerable < ToolChatMessage > CallAll ( IEnumerable < ChatToolCall > toolCalls , out List < string > ? failed )
227+ public async Task < ToolCallResult > CallAllWithErrors ( IEnumerable < ChatToolCall > toolCalls )
154228 {
155- failed = null ;
229+ List < string > ? failed = null ;
230+ bool isMcpTool = false ;
156231 var messages = new List < ToolChatMessage > ( ) ;
157232 foreach ( ChatToolCall toolCall in toolCalls )
158233 {
159234 if ( ! _methods . ContainsKey ( toolCall . FunctionName ) )
160235 {
161- if ( failed == null ) failed = new List < string > ( ) ;
162- failed . Add ( toolCall . FunctionName ) ;
163- continue ;
236+ if ( _mcpMethods . ContainsKey ( toolCall . FunctionName ) )
237+ {
238+ isMcpTool = true ;
239+ }
240+ else
241+ {
242+ failed ??= new List < string > ( ) ;
243+ failed . Add ( toolCall . FunctionName ) ;
244+ continue ;
245+ }
164246 }
165- var result = Call ( toolCall ) ;
247+
248+ var result = isMcpTool ? await CallMcp ( toolCall ) . ConfigureAwait ( false ) : Call ( toolCall ) ;
166249 messages . Add ( new ToolChatMessage ( toolCall . Id , result ) ) ;
167250 }
168- return messages ;
251+ return new ( messages , failed ) ;
169252 }
170253
171254 /// <summary>
0 commit comments