1- import { jsonSchema , type CoreMessage , type ToolSet , type LanguageModelV1 , type UserContent } from "ai" ;
2- import { getNamespacedData , type MCPClientManager , type NamespacedData } from "./client" ;
1+ import {
2+ jsonSchema ,
3+ type CoreMessage ,
4+ type ToolSet ,
5+ generateText ,
6+ type LanguageModelV1 ,
7+ } from "ai" ;
8+ import {
9+ getNamespacedData ,
10+ type MCPClientManager ,
11+ type NamespacedData ,
12+ } from "./client" ;
313import type { MCPClientConnection } from "./client-connection" ;
4- import type { Resource } from "@modelcontextprotocol/sdk/types.js"
14+ import type { Resource } from "@modelcontextprotocol/sdk/types.js" ;
15+ import { z } from "zod" ;
516
617/**
718 * A context router provides an interface for:
819 * - Filtering tools and resources
9- * - Exposes a system prompt based on the MCP state
20+ * - Exposes a system prompt based on MCP state
21+ *
22+ * This filtering and system prompt is intended to be determined by the internal messages state, set via
23+ * setMessages()
1024 */
11- export interface ContextRouter {
12- systemPrompt ( connManager : MCPClientManager ) : string
13-
14- listTools ( connManager : MCPClientManager ) : NamespacedData [ "tools" ]
15- getAITools ( connManager : MCPClientManager ) : ToolSet
16- listResources ( connManager : MCPClientManager ) : NamespacedData [ "resources" ]
25+ export abstract class ContextRouter {
26+ private _clientManager : MCPClientManager | undefined ;
27+
28+ get clientManager ( ) {
29+ if ( ! this . _clientManager ) {
30+ throw new Error (
31+ "Tried to get the client manager before it was set. Is the ContextRouter being correctly used in an MCPClientManager?"
32+ ) ;
33+ }
34+ return this . clientManager ;
35+ }
36+
37+ set clientManager ( clientManager : MCPClientManager ) {
38+ this . _clientManager = clientManager ;
39+ }
40+
41+ /**
42+ * Return the system prompt
43+ * @param clientManager
44+ */
45+ abstract systemPrompt ( ) : string ;
46+
47+ /**
48+ * Set internal context router state. This state is intended to allow
49+ * the ContextRouter to filter tools, resources, or construct the
50+ * system prompt.
51+ *
52+ * @param messages
53+ */
54+ abstract setMessages ( messages : CoreMessage [ ] ) : void | Promise < void > ;
55+
56+ /**
57+ * List tools from the client manager based on internal context router state
58+ * @param clientManager
59+ */
60+ abstract listTools ( ) : NamespacedData [ "tools" ] ;
61+
62+ /**
63+ * List AI tools from the client manager based on internal context router state. This could include
64+ * "synthetic" tools not sourced from an MCP server, such as a `read_resource` or `list_resources` tool.
65+ *
66+ * @param clientManager
67+ */
68+ abstract getAITools ( ) : ToolSet ;
69+
70+ /**
71+ * List tools from the client manager based on internal context router state
72+ * @param clientManager
73+ */
74+ abstract listResources ( ) : NamespacedData [ "resources" ] ;
1775}
1876
1977/**
20- * The base ContextRouter :
78+ * The BaseContextRouter :
2179 * - Does not filter tools or resources
22- * - Provides a system prompt
80+ * - Exposes the setMessages interface, but setting it is a no-op
81+ * - Provides a system prompt based on ALL tools & resources
2382 */
24- export class BaseContextRouter implements ContextRouter {
25- constructor ( public includeResources = true ) { }
83+ export class BaseContextRouter extends ContextRouter {
84+ constructor ( private includeResources = true ) {
85+ super ( ) ;
86+ }
87+
88+ setMessages ( _messages : CoreMessage [ ] ) : void { }
2689
27- listTools ( connManager : MCPClientManager ) {
28- return getNamespacedData ( connManager . mcpConnections , "tools" ) ;
90+ listTools ( ) {
91+ return getNamespacedData ( this . clientManager . mcpConnections , "tools" ) ;
2992 }
3093
31- getAITools ( connManager : MCPClientManager ) : ToolSet {
94+ getAITools ( ) : ToolSet {
3295 return Object . fromEntries (
33- this . listTools ( connManager ) . map ( ( tool ) => {
96+ this . listTools ( ) . map ( ( tool ) => {
3497 return [
3598 `${ tool . serverId } _${ tool . name } ` ,
3699 {
37100 parameters : jsonSchema ( tool . inputSchema ) ,
38101 description : tool . description ,
39102 execute : async ( args ) => {
40- const result = await connManager . callTool ( {
103+ const result = await this . clientManager . callTool ( {
41104 name : tool . name ,
42105 arguments : args ,
43106 serverId : tool . serverId ,
@@ -51,23 +114,23 @@ export class BaseContextRouter implements ContextRouter {
51114 } ,
52115 ] ;
53116 } )
54- )
117+ ) ;
55118 }
56119
57- listResources ( connManager : MCPClientManager ) {
58- return getNamespacedData ( connManager . mcpConnections , "resources" ) ;
120+ listResources ( ) {
121+ return getNamespacedData ( this . clientManager . mcpConnections , "resources" ) ;
59122 }
60123
61- systemPrompt ( connManager : MCPClientManager ) : string {
124+ systemPrompt ( ) : string {
62125 return `<integrations_list>
63126 You have access to multiple integrations via Model Context Protocol (MCP). These integrations provide you with tools which you can use to execute to complete tasks or retrieive information.
64127
65128 ${ this . includeResources && "Each integration, provides a list of resources, which are included in the list of integrations below." }
66129
67130 Here is a list of all of the integrations you have access to, with instructions if necessary:
68131
69- ${ Object . entries ( connManager . mcpConnections ) . map ( ( [ _id , conn ] ) => BaseContextRouter . serverContext ( conn , this . includeResources ) ) }
70- <integrations_list>`
132+ ${ Object . entries ( this . clientManager . mcpConnections ) . map ( ( [ _id , conn ] ) => BaseContextRouter . serverContext ( conn , this . includeResources ) ) }
133+ <integrations_list>` ;
71134 }
72135
73136 static serverContext ( conn : MCPClientConnection , includeResources : boolean ) {
@@ -77,7 +140,7 @@ export class BaseContextRouter implements ContextRouter {
77140 ${ includeResources && `<resources_list>${ conn . resources . map ( ( resource ) => BaseContextRouter . resourceContext ( resource ) ) } </resources_list>` }
78141<integration>` ;
79142 }
80-
143+
81144 static resourceContext ( resource : Resource ) {
82145 return `<resource>
83146 <name>${ resource . name } </name>
@@ -86,4 +149,153 @@ export class BaseContextRouter implements ContextRouter {
86149 <mimeType>${ resource . mimeType } </mimeType>
87150</resource>` ;
88151 }
89- }
152+ }
153+
154+ /**
155+ * The ModelHeuristicRouter:
156+ * - Filters tools and resources using an LLM
157+ */
158+ export class ModelHeuristicRouter extends BaseContextRouter {
159+ private tools : NamespacedData [ "tools" ] ;
160+ private resources : NamespacedData [ "resources" ] ;
161+
162+ constructor (
163+ private model : LanguageModelV1 ,
164+ private options : {
165+ toolLimit : number ;
166+ resourceLimit : number ;
167+ } = { toolLimit : 10 , resourceLimit : 5 }
168+ ) {
169+ super ( true ) ;
170+ this . tools = getNamespacedData ( this . clientManager . mcpConnections , "tools" ) ;
171+ this . resources = getNamespacedData (
172+ this . clientManager . mcpConnections ,
173+ "resources"
174+ ) ;
175+ }
176+
177+ async setMessages ( messages : CoreMessage [ ] ) : Promise < void > {
178+ this . tools = [ ] ;
179+ this . resources = [ ] ;
180+
181+ const tools = super . listTools ( ) ;
182+ const resources = super . listResources ( ) ;
183+
184+ await Promise . all ( [
185+ generateText ( {
186+ model : this . model ,
187+ system : this . toolSelectionPrompt ( tools ) ,
188+ messages,
189+ tools : {
190+ add_tool : {
191+ parameters : z . object ( {
192+ tool_name : z . string ( ) . describe ( "The exact tool name to add" ) ,
193+ } ) ,
194+ description : "Add a tool to context" ,
195+ execute : async ( { tool_name } ) => {
196+ const toolToAdd = tools . find ( ( tool ) =>
197+ tool . name . includes ( tool_name )
198+ ) ;
199+ if ( ! toolToAdd ) {
200+ return "Failed to find tool by the name `tool_name`" ;
201+ }
202+ if ( this . tools . length >= this . options . toolLimit ) {
203+ return "Failed to add tool. There are already too many active tools." ;
204+ }
205+ this . tools . push ( toolToAdd ) ;
206+ return "Successfully added tool" ;
207+ } ,
208+ } ,
209+ } ,
210+ } ) ,
211+ generateText ( {
212+ model : this . model ,
213+ system : this . resourceSelectionPrompt ( resources ) ,
214+ messages,
215+ tools : {
216+ add_resource : {
217+ parameters : z . object ( {
218+ resource_uri : z . string ( ) ,
219+ } ) ,
220+ description : "Add a tool to context" ,
221+ execute : async ( { resource_uri } , options ) => {
222+ const resourceToAdd = resources . find (
223+ ( resource ) => resource . uri === resource_uri
224+ ) ;
225+ if ( ! resourceToAdd ) {
226+ return `Failed to find resource by the uri "${ resource_uri } "` ;
227+ }
228+ if ( this . resources . length >= this . options . resourceLimit ) {
229+ return "Failed to add resource. There are already too many active resources." ;
230+ }
231+ this . resources . push ( resourceToAdd ) ;
232+ return "Successfully added resource" ;
233+ } ,
234+ } ,
235+ } ,
236+ } ) ,
237+ ] ) ;
238+ }
239+
240+ listTools ( ) {
241+ return this . tools ;
242+ }
243+
244+ listResources ( ) {
245+ return this . resources ;
246+ }
247+
248+ toolSelectionPrompt ( tools : NamespacedData [ "tools" ] ) {
249+ return `
250+ You are an expert at selecting relevant tools for a conversation with a large language model. You are given access to only one tool: "add_tool".
251+
252+ You will be provided a list of tools, their descriptions, and a series of messages below.
253+ Based on the messages, call the tool "add_tool" with the exact tool_name of the tool you would like to add.
254+
255+ * You MUST use "add_tool" with only the tool_name argument
256+ * You SHOULD NOT ask for permission to use the "add_tool" call.
257+ * You MUST NOT respond to the attached messages below, ONLY use the system prompt
258+ * You MUST NOT attempt to call any other tools
259+ * Limit the amount of tools selected to ${ this . options . toolLimit }
260+
261+ <tool_options>
262+ ${ tools . map ( ( tool ) => {
263+ return `
264+ <tool>
265+ <name>${ tool . name } </name>
266+ <description>${ tool . description } </description>
267+ </tool>
268+ ` ;
269+ } ) }
270+ </tool_options>
271+ ` ;
272+ }
273+
274+ resourceSelectionPrompt ( resources : NamespacedData [ "resources" ] ) {
275+ return `
276+ You are an expert at selecting relevant resources for a conversation with a large language model. You are given access to only one tool: "add_resource".
277+
278+ You will be provided a list of resources, their descriptions, and a series of messages below.
279+ Based on the messages, call the tool "add_resource" with the exact resource_uri of the resource you would like to add.
280+
281+ * You MUST use "add_resource" with only the resource_uri argument
282+ * You SHOULD NOT ask for permission to use the "add_resource" call.
283+ * You MUST NOT respond to the attached messages below, ONLY use the system prompt
284+ * You MUST NOT attempt to call any other tools
285+ * Limit the amount of resources selected to ${ this . options . resourceLimit }
286+
287+ <resource_options>
288+ ${ resources . map ( ( resource ) => {
289+ return `
290+ <resource>
291+ <uri>${ resource . uri } </uri>
292+ <name>${ resource . name } </name>
293+ <mimeType>${ resource . mimeType } </mimeType>
294+ <description>${ resource . description } </description>
295+ </resource>
296+ ` ;
297+ } ) }
298+ </resource_options>
299+ ` ;
300+ }
301+ }
0 commit comments