Skip to content

fix(ai-proxy): route Forest connectors via tool-provider split in AiClient#1599

Merged
christophebrun-forest merged 2 commits into
feat/prd-214-server-step-mapperfrom
fix/prd-400-forest-connectors-loadremotetools
May 28, 2026
Merged

fix(ai-proxy): route Forest connectors via tool-provider split in AiClient#1599
christophebrun-forest merged 2 commits into
feat/prd-214-server-step-mapperfrom
fix/prd-400-forest-connectors-loadremotetools

Conversation

@christophebrun-forest
Copy link
Copy Markdown
Member

@christophebrun-forest christophebrun-forest commented May 27, 2026

Summary

  • AiClient.loadRemoteTools now delegates to createToolProviders (same path as Router and ToolSourceChecker), so Forest-hosted connectors (Zendesk, Kolar, Snowflake) are routed to ForestIntegrationClient instead of being forwarded to MultiServerMCPClient.
  • Before this fix, any workflow run whose getMcpServerConfigs() response included a Forest connector crashed at step setup with ZodError invalid_union mcpServers.<name> — Forest connector entries have neither command+args (stdio) nor url (HTTP), so the @langchain/mcp-adapters schema rejected them.
  • Propagates the cleaner Record<string, ToolConfig> shape through AiModelPort, the three adapters (ServerAiAdapter, AiClientAdapter, AlwaysErrorAiModelPort), Runner.fetchRemoteTools (drops the now-redundant { configs } wrapping), and WorkflowPort.getMcpServerConfigs (was mistyped as McpServers while the orchestrator already returns a mixed record).

Fixes PRD-400.

Test plan

  • yarn workspace @forestadmin/ai-proxy test — 400 passing (new regression test in ai-client.test.ts covers Forest connector configs lacking MCP transport fields).
  • yarn workspace @forestadmin/workflow-executor test — 786 passing.
  • yarn workspace @forestadmin/ai-proxy lint && yarn workspace @forestadmin/workflow-executor lint — clean (preexisting warnings only).
  • yarn workspace @forestadmin/ai-proxy build && yarn workspace @forestadmin/workflow-executor build — clean.
  • Smoke test against the dev orchestrator with a workflow that has a Zendesk step: the executor must load the Zendesk tools and the step must reach AI selection without the Zod crash.

🤖 Generated with Claude Code

Note

Route Forest connectors via a generic tool-provider split in AiClient

  • Replaces the single McpClient field in AiClient with a toolProviders array, allowing multiple provider kinds (e.g. MCP and Forest) to be loaded together via createToolProviders.
  • loadRemoteTools now accepts Record<string, ToolConfig> instead of McpConfiguration, creates providers for each config entry, flattens their tools, and disposes any previously loaded providers before replacing them.
  • All port interfaces (AiModelPort, WorkflowPort) and adapters are updated to use Record<string, ToolConfig> end-to-end, removing the McpServers wrapper type.
  • disposeToolProviders uses Promise.allSettled to isolate per-provider failures and log each rejection individually without interrupting others.

Macroscope summarized fcc1305.

…lient

AiClient.loadRemoteTools was instantiating McpClient directly, forwarding
every config (including ForestIntegrationConfig entries like Zendesk) to
MultiServerMCPClient. Forest connectors lack the stdio/HTTP transport
fields, so @langchain/mcp-adapters threw a Zod union error and any
workflow run touching a Forest-hosted connector crashed before the step
could execute. Delegate to createToolProviders so Forest connectors are
routed to ForestIntegrationClient — same path Router and ToolSourceChecker
already use. Fixes PRD-400.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented May 27, 2026

PRD-400

…test

The previous "does not crash on Forest connector configs" test in
ai-client.test.ts mocked createToolProviders, so it would have passed
against the buggy code too. Replace it with a dedicated routing test
that uses the real createToolProviders and asserts McpClient is never
constructed with a Forest connector entry — which is the exact bug
PRD-400 fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qltysh
Copy link
Copy Markdown

qltysh Bot commented May 27, 2026

Qlty


Coverage Impact

Unable to calculate total coverage change because base branch coverage was not found.

Modified Files with Diff Coverage (5)

RatingFile% DiffUncovered Line #s
New Coverage rating: A
packages/workflow-executor/src/runner.ts100.0%
New Coverage rating: A
.../workflow-executor/src/adapters/forest-server-workflow-port.ts100.0%
New Coverage rating: B
packages/workflow-executor/src/adapters/server-ai-adapter.ts100.0%
New Coverage rating: C
packages/workflow-executor/src/adapters/ai-client-adapter.ts100.0%
New Coverage rating: A
packages/ai-proxy/src/ai-client.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

dispose: disposeMock,
} as unknown as McpClient),
);
it('disposes every stored provider', async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Opus 4.7 · Should fix

The dispose-failure tests all use a single provider, and this two-provider test has both dispose resolve — so nothing pins the Promise.allSettled isolation contract that this PR introduces. If disposeToolProviders were refactored back to Promise.all, a leading rejection would skip the second provider's disposal and every test here would still pass. Add a case with two providers where the first dispose rejects and the second resolves, then assert the second's dispose still ran and the logger was called once with the rejected reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(Claude's human here) - Good safeguard to have but it's more of a preferential for me

this.mcpClient = newClient;
const providers = createToolProviders(configs, this.logger);
const toolsByProvider = await Promise.all(providers.map(p => p.loadTools()));
this.toolProviders = providers;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Opus 4.7 · Preferential

this.toolProviders = providers runs only after await Promise.all(...) resolves. If any provider's loadTools() rejects, the created providers are never stored, so a provider that already opened connections can't be disposed via closeConnections(). This is currently latent — ForestIntegrationClient.loadTools opens nothing and McpClient.loadTools swallows per-server errors rather than rejecting — and it matches the previous single-McpClient pattern, so not a regression. Defensive option: assign before awaiting, or wrap the load in a try/catch that disposes the freshly-created providers before rethrowing. (The test at line 229 currently codifies the drop-on-failure behaviour.)

}

async getMcpServerConfigs(): Promise<McpServers> {
async getMcpServerConfigs(): Promise<Record<string, ToolConfig>> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getToolServerConfigs would be more consistent, but I think it's out of scope for this PR. If you agree, perhaps file a refactor ticket ?

@christophebrun-forest christophebrun-forest merged commit a5f01b5 into feat/prd-214-server-step-mapper May 28, 2026
29 of 30 checks passed
@christophebrun-forest christophebrun-forest deleted the fix/prd-400-forest-connectors-loadremotetools branch May 28, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants