-
Notifications
You must be signed in to change notification settings - Fork 12
feat(mcp-server): dispatch mounted tool calls to the agent in-process #1743
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?
Changes from all commits
8c7b962
c476514
c26d56b
0507fb9
0327a3c
a1bcaf1
4ba880e
d6a46e2
fec6e70
ae3a5c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium agent-nodejs/packages/agent/src/framework-mounter.ts Lines 73 to 74 in fec6e70
protected async remount(router: Router): Promise<void> {
+ this.inProcessDispatcher.setHandler(null);
for (const task of this.onEachStart) await task(router); // eslint-disable-line no-await-in-loop🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import type { Logger } from '@forestadmin/datasource-toolkit'; | ||
| import type { InProcessAgentDispatcher } from '@forestadmin/mcp-server'; | ||
| import type { RequestListener } from 'http'; | ||
|
|
||
| import inject, { type InjectOptions, type InjectPayload } from 'light-my-request'; | ||
|
|
||
| export type InProcessRequest = { | ||
| method: string; | ||
| path: string; | ||
| headers: Record<string, string>; | ||
| query?: Record<string, unknown>; | ||
| payload?: unknown; | ||
| timeoutMs?: number; | ||
| }; | ||
|
|
||
| // Superagent-shaped so agent-client's HttpRequester helpers parse it identically. | ||
| export type InProcessResponse = { status: number; body: unknown; text: string }; | ||
|
|
||
| // Matches superagent's HTTP path, which times out after 10s (HttpRequester.query/stream). | ||
| const DEFAULT_TIMEOUT_MS = 10_000; | ||
|
|
||
| function toStringQuery(query: Record<string, unknown>): Record<string, string> { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| const result: Record<string, string> = {}; | ||
|
|
||
| for (const [key, value] of Object.entries(query)) { | ||
| if (value !== undefined) result[key] = String(value); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Dispatches an agent-client request into the agent's own Koa stack in-memory (no socket), so a | ||
| * mounted MCP server's tool calls run through the real auth/permission/logging middleware. | ||
| * `setHandler` is refreshed on every (re)mount, so a captured reference never goes stale. | ||
| */ | ||
| export default class InProcessDispatcher implements InProcessAgentDispatcher { | ||
| private handler: RequestListener | null = null; | ||
|
|
||
| constructor(private readonly logger?: Logger) {} | ||
|
|
||
| setHandler(handler: RequestListener | null): void { | ||
| this.handler = handler; | ||
| } | ||
|
|
||
| async request({ | ||
| method, | ||
| path, | ||
| headers, | ||
| query, | ||
| payload, | ||
| timeoutMs = DEFAULT_TIMEOUT_MS, | ||
| }: InProcessRequest): Promise<InProcessResponse> { | ||
| if (!this.handler) { | ||
| throw new Error('Cannot dispatch to the agent in-process: it is not mounted yet.'); | ||
| } | ||
|
|
||
| // No socket to abort, so a hung agent handler would hang the tool call forever without this. | ||
| const response = await this.injectWithTimeout( | ||
| { | ||
| method: method.toUpperCase() as InjectOptions['method'], | ||
| url: path, | ||
| headers, | ||
| ...(query && { query: toStringQuery(query) }), | ||
| ...(payload !== undefined && { payload: payload as InjectPayload }), | ||
| }, | ||
| timeoutMs, | ||
| ); | ||
|
|
||
| return { status: response.statusCode, body: this.parseBody(response), text: response.payload }; | ||
| } | ||
|
|
||
| private async injectWithTimeout(options: InjectOptions, timeoutMs: number) { | ||
| let timer: NodeJS.Timeout | undefined; | ||
| const timeout = new Promise<never>((_, reject) => { | ||
| timer = setTimeout( | ||
| () => reject(new Error(`In-process dispatch timed out after ${timeoutMs}ms`)), | ||
| timeoutMs, | ||
| ); | ||
| }); | ||
|
|
||
| try { | ||
| return await Promise.race([inject(this.handler as RequestListener, options), timeout]); | ||
| } finally { | ||
| if (timer) clearTimeout(timer); | ||
| } | ||
| } | ||
|
|
||
| private parseBody(response: { payload: string }): unknown { | ||
| if (response.payload === '') return undefined; | ||
|
|
||
| try { | ||
| return JSON.parse(response.payload); | ||
| } catch (error) { | ||
| this.logger?.('Warn', `In-process dispatch received a non-JSON response body: ${error}`); | ||
|
|
||
| return undefined; | ||
| } | ||
| } | ||
| } | ||
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.
🟠 High
agent-nodejs/packages/agent/src/agent.ts
Line 433 in fec6e70
Passing
agentDispatcher: this.getInProcessDispatcher()routes MCP tool calls through the in-process dispatcher, which only mountsdriverRouter.routes()and omits thebodyParser(...)middleware thatAgent.getRouter()applies for normal HTTP traffic. As a result, any MCP tool that sends a JSON request body (e.g.create,update,delete,associate,dissociate,getActionForm,executeAction) reaches route handlers that readcontext.request.bodywhile it is still unset, so those requests fail or are processed with missing inputs — even though the same operations work correctly over HTTP. Consider applying the samebodyParserconfiguration to the in-process dispatcher path so MCP tool calls parse JSON bodies before reaching the driver routes.🚀 Reply "fix it for me" or copy this AI Prompt for your agent: