1+ using Microsoft . Extensions . AI ;
2+ using Microsoft . Extensions . DependencyInjection ;
3+ using ModelContextProtocol . Protocol . Types ;
4+ using ModelContextProtocol . Server ;
5+ using ModelContextProtocol . Utils ;
6+ using ModelContextProtocol . Utils . Json ;
7+ using System . Reflection ;
8+ using System . Text . Json ;
9+
10+ namespace ModelContextProtocol ;
11+
12+ /// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
13+ internal sealed class AIFunctionMcpServerTool : McpServerTool
14+ {
15+ /// <summary>Key used temporarily for flowing request context into an AIFunction.</summary>
16+ /// <remarks>This will be replaced with use of AIFunctionArguments.Context.</remarks>
17+ private const string RequestContextKey = "__temporary_RequestContext" ;
18+
19+ /// <summary>
20+ /// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
21+ /// </summary>
22+ public static new AIFunctionMcpServerTool Create ( Delegate method , IServiceProvider ? services = null )
23+ {
24+ Throw . IfNull ( method ) ;
25+
26+ return Create ( method . Method , method . Target , services ) ;
27+ }
28+
29+ /// <summary>
30+ /// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
31+ /// </summary>
32+ public static new AIFunctionMcpServerTool Create ( MethodInfo method , object ? target = null , IServiceProvider ? services = null )
33+ {
34+ Throw . IfNull ( method ) ;
35+
36+ // TODO: Once this repo consumes a new build of Microsoft.Extensions.AI containing
37+ // https://github.com/dotnet/extensions/pull/6158,
38+ // https://github.com/dotnet/extensions/pull/6162, and
39+ // https://github.com/dotnet/extensions/pull/6175, switch over to using the real
40+ // AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by
41+ // which the arguments are passed.
42+
43+ return Create ( TemporaryAIFunctionFactory . Create ( method , target , new TemporaryAIFunctionFactoryOptions ( )
44+ {
45+ Name = method . GetCustomAttribute < McpServerToolAttribute > ( ) ? . Name ,
46+ MarshalResult = static ( result , _ , cancellationToken ) => Task . FromResult ( result ) ,
47+ ConfigureParameterBinding = pi =>
48+ {
49+ if ( pi . ParameterType == typeof ( RequestContext < CallToolRequestParams > ) )
50+ {
51+ return new ( )
52+ {
53+ ExcludeFromSchema = true ,
54+ BindParameter = ( pi , args ) => GetRequestContext ( args ) ,
55+ } ;
56+ }
57+
58+ if ( pi . ParameterType == typeof ( IMcpServer ) )
59+ {
60+ return new ( )
61+ {
62+ ExcludeFromSchema = true ,
63+ BindParameter = ( pi , args ) => GetRequestContext ( args ) ? . Server ,
64+ } ;
65+ }
66+
67+ // We assume that if the services used to create the tool support a particular type,
68+ // so too do the services associated with the server. This is the same basic assumption
69+ // made in ASP.NET.
70+ if ( services is not null &&
71+ services . GetService < IServiceProviderIsService > ( ) is { } ispis &&
72+ ispis . IsService ( pi . ParameterType ) )
73+ {
74+ return new ( )
75+ {
76+ ExcludeFromSchema = true ,
77+ BindParameter = ( pi , args ) =>
78+ GetRequestContext ( args ) ? . Server ? . Services ? . GetService ( pi . ParameterType ) ??
79+ ( pi . HasDefaultValue ? null :
80+ throw new ArgumentException ( "No service of the requested type was found." ) ) ,
81+ } ;
82+ }
83+
84+ if ( pi . GetCustomAttribute < FromKeyedServicesAttribute > ( ) is { } keyedAttr )
85+ {
86+ return new ( )
87+ {
88+ ExcludeFromSchema = true ,
89+ BindParameter = ( pi , args ) =>
90+ ( GetRequestContext ( args ) ? . Server ? . Services as IKeyedServiceProvider ) ? . GetKeyedService ( pi . ParameterType , keyedAttr . Key ) ??
91+ ( pi . HasDefaultValue ? null :
92+ throw new ArgumentException ( "No service of the requested type was found." ) ) ,
93+ } ;
94+ }
95+
96+ return default ;
97+
98+ static RequestContext < CallToolRequestParams > ? GetRequestContext ( IReadOnlyDictionary < string , object ? > args )
99+ {
100+ if ( args . TryGetValue ( RequestContextKey , out var orc ) &&
101+ orc is RequestContext < CallToolRequestParams > requestContext )
102+ {
103+ return requestContext ;
104+ }
105+
106+ return null ;
107+ }
108+ } ,
109+ } ) ) ;
110+ }
111+
112+ /// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
113+ public static new AIFunctionMcpServerTool Create ( AIFunction function )
114+ {
115+ Throw . IfNull ( function ) ;
116+
117+ return new AIFunctionMcpServerTool ( function ) ;
118+ }
119+
120+ /// <summary>Gets the <see cref="AIFunction"/> wrapped by this tool.</summary>
121+ internal AIFunction AIFunction { get ; }
122+
123+ /// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
124+ private AIFunctionMcpServerTool ( AIFunction function )
125+ {
126+ AIFunction = function ;
127+ ProtocolTool = new ( )
128+ {
129+ Name = function . Name ,
130+ Description = function . Description ,
131+ InputSchema = function . JsonSchema ,
132+ } ;
133+ }
134+
135+ /// <inheritdoc />
136+ public override string ToString ( ) => AIFunction . ToString ( ) ;
137+
138+ /// <inheritdoc />
139+ public override Tool ProtocolTool { get ; }
140+
141+ /// <inheritdoc />
142+ public override async Task < CallToolResponse > InvokeAsync (
143+ RequestContext < CallToolRequestParams > request , CancellationToken cancellationToken = default )
144+ {
145+ Throw . IfNull ( request ) ;
146+
147+ cancellationToken . ThrowIfCancellationRequested ( ) ;
148+
149+ // TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context.
150+ Dictionary < string , object ? > arguments = request . Params ? . Arguments is IDictionary < string , object ? > existingArgs ?
151+ new ( existingArgs ) :
152+ [ ] ;
153+ arguments [ RequestContextKey ] = request ;
154+
155+ object ? result ;
156+ try
157+ {
158+ result = await AIFunction . InvokeAsync ( arguments , cancellationToken ) . ConfigureAwait ( false ) ;
159+ }
160+ catch ( Exception e ) when ( e is not OperationCanceledException )
161+ {
162+ return new CallToolResponse ( )
163+ {
164+ IsError = true ,
165+ Content = [ new ( ) { Text = e . Message , Type = "text" } ] ,
166+ } ;
167+ }
168+
169+ switch ( result )
170+ {
171+ case null :
172+ return new ( )
173+ {
174+ Content = [ ]
175+ } ;
176+
177+ case string text :
178+ return new ( )
179+ {
180+ Content = [ new ( ) { Text = text , Type = "text" } ]
181+ } ;
182+
183+ case TextContent textContent :
184+ return new ( )
185+ {
186+ Content = [ new ( ) { Text = textContent . Text , Type = "text" } ]
187+ } ;
188+
189+ case DataContent dataContent :
190+ return new ( )
191+ {
192+ Content = [ new ( )
193+ {
194+ Data = dataContent . GetBase64Data ( ) ,
195+ MimeType = dataContent . MediaType ,
196+ Type = dataContent . HasTopLevelMediaType ( "image" ) ? "image" : "resource" ,
197+ } ]
198+ } ;
199+
200+ case string [ ] texts :
201+ return new ( )
202+ {
203+ Content = texts
204+ . Select ( x => new Content ( ) { Type = "text" , Text = x ?? string . Empty } )
205+ . ToList ( )
206+ } ;
207+
208+ // TODO https://github.com/modelcontextprotocol/csharp-sdk/issues/69:
209+ // Add specialization for annotations.
210+
211+ default :
212+ return new ( )
213+ {
214+ Content = [ new ( )
215+ {
216+ Text = JsonSerializer . Serialize ( result , McpJsonUtilities . DefaultOptions . GetTypeInfo ( typeof ( object ) ) ) ,
217+ Type = "text"
218+ } ]
219+ } ;
220+ }
221+ }
222+ }
0 commit comments