Skip to content

refactor(ai-proxy): align McpClient API with main branch - #1527

Closed
matthv wants to merge 29 commits into
mainfrom
refactor/ai-proxy-align-mcp-client
Closed

refactor(ai-proxy): align McpClient API with main branch#1527
matthv wants to merge 29 commits into
mainfrom
refactor/ai-proxy-align-mcp-client

Conversation

@matthv

@matthv matthv commented Apr 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Rename McpClient.closeConnections()dispose()
  • Rename McpClient.testConnections()checkConnection()
  • Remove McpClient.tools property, loadTools() returns a local array instead
  • Add McpServers type export
  • Rename mcpServerConfigstoolConfigs in create-ai-provider
  • Update all tests accordingly

Pure renaming refactor, no logic changes.

Test plan

  • yarn workspace @forestadmin/ai-proxy build — compiles
  • yarn workspace @forestadmin/ai-proxy test — 14/14 suites, 210 tests pass
  • yarn workspace @forestadmin/workflow-executor test — 427 tests pass

🤖 Generated with Claude Code

Note

Align McpClient API with main branch and introduce the workflow-executor package

  • Adds the new @forestadmin/workflow-executor package with a full step-execution engine: Runner, store adapters (InMemoryStore, DatabaseStore), HTTP server (ExecutorHttpServer), and step executors for Condition, ReadRecord, UpdateRecord, TriggerAction, LoadRelatedRecord, and MCP step types.
  • Introduces buildInMemoryExecutor and buildDatabaseExecutor factory functions in build-workflow-executor.ts that wire up all dependencies and handle SIGTERM/SIGINT for graceful shutdown.
  • Adds AiClient, createBaseChatModel, and getAiConfiguration to packages/ai-proxy and renames the closeConnection method to dispose on McpClient.
  • Exports ServerUtils from the forestadmin-client package index and adds the workflow-executor package to CI in build.yml.
  • Behavioral Change: McpClient.closeConnection is renamed to dispose; any existing callers must update to the new method name.

Macroscope summarized d43f168.

matthv and others added 24 commits March 17, 2026 15:00
…premature deps, add smoke test

- Rewrite CLAUDE.md with project overview and architecture principles, remove changelog
- Remove unused dependencies (ai-proxy, sequelize, zod) per YAGNI
- Add smoke test so CI passes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… document system architecture

- Lint now covers src and test directories
- Replace require() with import, use stronger assertion (toHaveLength)
- Add System Architecture section describing Front/Orchestrator/Executor/Agent
- Mark Architecture Principles as planned (not yet implemented)
- Remove redundant test/.gitkeep
- Make index.ts a valid module with export {}

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…erver (#1504)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: alban bertolini <albanb@forestadmin.com>
…ain (#1512)

Co-authored-by: alban bertolini <albanb@forestadmin.com>
- Remove McpClient.tools property, loadTools() returns local array
- Rename closeConnections() → dispose()
- Rename testConnections() → checkConnection()
- Add McpServers type export
- Rename mcpServerConfigs → toolConfigs in create-ai-provider
- Update all tests accordingly

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

qltysh Bot commented Apr 2, 2026

Copy link
Copy Markdown

15 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 14): createWorkflowExecutor 7
qlty Structure Function with many returns (count = 4): getAiConfiguration 4
qlty Structure Deeply nested control flow (level = 4) 2
qlty Duplication Found 16 lines of similar code in 2 locations (mass = 92) 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

function resolveMcpConfigs(args: Parameters<AiRouter['route']>[0]): McpConfiguration | undefined {

resolveMcpConfigs accesses args.toolConfigs, but the AiRouter['route'] interface only defines mcpServerConfigs?: unknown. Since toolConfigs doesn't exist on the type, the property will always be undefined at runtime, causing all MCP server configurations to be silently discarded.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/ai-proxy/src/create-ai-provider.ts around line 9:

`resolveMcpConfigs` accesses `args.toolConfigs`, but the `AiRouter['route']` interface only defines `mcpServerConfigs?: unknown`. Since `toolConfigs` doesn't exist on the type, the property will always be `undefined` at runtime, causing all MCP server configurations to be silently discarded.

Evidence trail:
packages/ai-proxy/src/create-ai-provider.ts:15 - accesses `args.toolConfigs`. packages/agent-toolkit/src/interfaces/ai.ts:27 - interface defines `mcpServerConfigs?: unknown`, not `toolConfigs`. packages/agent/src/routes/ai/ai-proxy.ts:36 - real caller passes `mcpServerConfigs`. packages/ai-proxy/test/create-ai-provider.test.ts:68 - tests pass `toolConfigs` directly bypassing interface type checking.

Comment on lines +440 to +448
// validated against the AI response. This guard is the sole runtime enforcement.
if (recordIndex < 0 || recordIndex > maxIndex) {
throw new InvalidAIResponseError(
`AI selected record index ${recordIndex} which is out of range (0-${maxIndex})`,
);
}

return recordIndex;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low executors/load-related-record-step-executor.ts:440

The bounds check at lines 442-446 accepts non-integer values like 1.5 because it only tests recordIndex < 0 and recordIndex > maxIndex. Since 1.5 satisfies both conditions, it passes validation, but candidates[1.5] returns undefined and the subsequent relatedData[bestIndex].recordId access throws a TypeError. Consider adding Number.isInteger(recordIndex) to the validation to reject non-integer indices before they cause an out-of-bounds array access.

    // NOTE: The Zod schema's .min(0).max(maxIndex) shapes the tool prompt only — it is NOT
    // validated against the AI response. This guard is the sole runtime enforcement.
-    if (recordIndex < 0 || recordIndex > maxIndex) {
+    if (!Number.isInteger(recordIndex) || recordIndex < 0 || recordIndex > maxIndex) {
       throw new InvalidAIResponseError(
-        `AI selected record index ${recordIndex} which is out of range (0-${maxIndex})`,
+        `AI selected record index ${recordIndex} which is out of range (0-${maxIndex}) or not an integer`,
       );
     }
 
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/executors/load-related-record-step-executor.ts around lines 440-448:

The bounds check at lines 442-446 accepts non-integer values like `1.5` because it only tests `recordIndex < 0` and `recordIndex > maxIndex`. Since `1.5` satisfies both conditions, it passes validation, but `candidates[1.5]` returns `undefined` and the subsequent `relatedData[bestIndex].recordId` access throws a `TypeError`. Consider adding `Number.isInteger(recordIndex)` to the validation to reject non-integer indices before they cause an out-of-bounds array access.

Evidence trail:
packages/workflow-executor/src/executors/load-related-record-step-executor.ts lines 440-446 (bounds check with explicit comment about Zod not validating), line 413 (Zod schema with .int()), lines 223-227 (selectBestRecordIndex called), line 236 (`relatedData[bestIndex]` access), lines 450-455 (toRecordRef accessing properties on data parameter)

Comment on lines +41 to +46
function extractRecordId(
primaryKeyFields: string[],
record: Record<string, unknown>,
): Array<string | number> {
return primaryKeyFields.map(field => record[field] as string | number);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low adapters/agent-client-agent-port.ts:41

When getRelatedData is called with a fields parameter that excludes the primary key fields, extractRecordId returns an array of undefined values cast to string | number. Downstream code expecting valid IDs receives corrupted data because the cast suppresses the type error. Consider validating that all primary key fields exist in the record before mapping, or ensuring primary key fields are always included in the query.

-function extractRecordId(
-  primaryKeyFields: string[],
-  record: Record<string, unknown>,
-): Array<string | number> {
-  return primaryKeyFields.map(field => record[field] as string | number);
+function extractRecordId(
+  primaryKeyFields: string[],
+  record: Record<string, unknown>,
+): Array<string | number> {
+  return primaryKeyFields.map(field => {
+    const value = record[field];
+    if (value === undefined) {
+      throw new Error(`Primary key field '${field}' missing from record`);
+    }
+    return value as string | number;
+  });
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/adapters/agent-client-agent-port.ts around lines 41-46:

When `getRelatedData` is called with a `fields` parameter that excludes the primary key fields, `extractRecordId` returns an array of `undefined` values cast to `string | number`. Downstream code expecting valid IDs receives corrupted data because the cast suppresses the type error. Consider validating that all primary key fields exist in the record before mapping, or ensuring primary key fields are always included in the query.

Evidence trail:
- packages/workflow-executor/src/adapters/agent-client-agent-port.ts lines 39-43: `extractRecordId` function casts `record[field]` (which can be `undefined`) to `string | number`
- packages/workflow-executor/src/adapters/agent-client-agent-port.ts lines 81-96: `getRelatedData` passes `fields` directly to query without ensuring primary key fields are included
- packages/workflow-executor/src/ports/agent-port.ts lines 14-19: `GetRelatedDataQuery.fields` is optional with no constraints on content
- packages/workflow-executor/src/types/record.ts lines 35-37: `RecordData.recordId` expects `Array<string | number>` but could receive `undefined` values due to the cast

Comment on lines +12 to +15
pendingStepExecutionForRun: (runId: string) =>
`/liana/v1/workflow-step-executions/pending?runId=${encodeURIComponent(runId)}`,
updateStepExecution: (runId: string) => `/liana/v1/workflow-step-executions/${runId}/complete`,
collectionSchema: (collectionName: string) => `/liana/v1/collections/${collectionName}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low adapters/forest-server-workflow-port.ts:12

ROUTES.updateStepExecution(runId) and ROUTES.collectionSchema(collectionName) do not encode their path parameters, so values containing /, ?, or # produce malformed URLs that either 404 or route to the wrong endpoint. ROUTES.pendingStepExecutionForRun already uses encodeURIComponent for the same purpose.

-  updateStepExecution: (runId: string) => `/liana/v1/workflow-step-executions/${runId}/complete`,
-  collectionSchema: (collectionName: string) => `/liana/v1/collections/${collectionName}`,
+  updateStepExecution: (runId: string) => `/liana/v1/workflow-step-executions/${encodeURIComponent(runId)}/complete`,
+  collectionSchema: (collectionName: string) => `/liana/v1/collections/${encodeURIComponent(collectionName)}`,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/workflow-executor/src/adapters/forest-server-workflow-port.ts around lines 12-15:

`ROUTES.updateStepExecution(runId)` and `ROUTES.collectionSchema(collectionName)` do not encode their path parameters, so values containing `/`, `?`, or `#` produce malformed URLs that either 404 or route to the wrong endpoint. `ROUTES.pendingStepExecutionForRun` already uses `encodeURIComponent` for the same purpose.

Evidence trail:
packages/workflow-executor/src/adapters/forest-server-workflow-port.ts lines 11-16: `pendingStepExecutionForRun` uses `encodeURIComponent(runId)` while `updateStepExecution` and `collectionSchema` directly interpolate their parameters without encoding.

Comment thread packages/workflow-executor/src/adapters/agent-client-agent-port.ts
matthv and others added 2 commits April 2, 2026 10:26
Merge main into feature branch, resolve conflicts by taking main's
versions of router.ts, create-ai-provider.ts and their tests.
Bump workflow-executor internal deps to match main.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
return model;
}

async loadRemoteTools(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low src/ai-client.ts:39

loadRemoteTools is not thread-safe: if called concurrently, two calls can both create McpClient instances, but only the second one is stored in this.mcpClient. The first client is orphaned and never disposed, leaking its connections. Consider adding a lock or check-and-set pattern to ensure only one client creation proceeds at a time.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/ai-proxy/src/ai-client.ts around line 39:

`loadRemoteTools` is not thread-safe: if called concurrently, two calls can both create `McpClient` instances, but only the second one is stored in `this.mcpClient`. The first client is orphaned and never disposed, leaking its connections. Consider adding a lock or check-and-set pattern to ensure only one client creation proceeds at a time.

Evidence trail:
packages/ai-proxy/src/ai-client.ts lines 39-48 (REVIEWED_COMMIT): `loadRemoteTools` method showing the race condition - `await newClient.loadTools()` creates a suspension point between client creation (line 44) and assignment to `this.mcpClient` (line 46), allowing concurrent calls to create orphaned clients

@qltysh

qltysh Bot commented Apr 2, 2026

Copy link
Copy Markdown

Qlty

Coverage Impact

⬆️ Merging this pull request will increase total coverage on main by 0.13%.

Modified Files with Diff Coverage (34)

RatingFile% DiffUncovered Line #s
Coverage rating: A Coverage rating: A
packages/forestadmin-client/src/index.ts100.0%
Coverage rating: A Coverage rating: A
packages/ai-proxy/src/index.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/executors/safe-agent-port.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/adapters/console-logger.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/executors/step-executor-factory.ts100.0%
New file Coverage rating: A
...ow-executor/src/executors/load-related-record-step-executor.ts99.1%146
New file Coverage rating: A
...orkflow-executor/src/executors/summary/step-summary-builder.ts100.0%
New file Coverage rating: B
packages/workflow-executor/src/build-workflow-executor.ts83.1%97-114
New file Coverage rating: A
packages/ai-proxy/src/get-ai-configuration.ts100.0%
New file Coverage rating: A
...s/workflow-executor/src/executors/read-record-step-executor.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/executors/record-step-executor.ts100.0%
New file Coverage rating: A
packages/ai-proxy/src/validate-ai-configurations.ts100.0%
New file Coverage rating: A
...ges/workflow-executor/src/executors/condition-step-executor.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/executors/base-step-executor.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/http/executor-http-server.ts98.9%119
New file Coverage rating: A
.../workflow-executor/src/adapters/forest-server-workflow-port.ts100.0%
New file Coverage rating: A
...workflow-executor/src/executors/update-record-step-executor.ts100.0%
New file Coverage rating: A
packages/ai-proxy/src/create-base-chat-model.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/pending-data-validators.ts100.0%
New file Coverage rating: A
...-executor/src/executors/trigger-record-action-step-executor.ts100.0%
New file Coverage rating: A
packages/ai-proxy/src/ai-client.ts100.0%
New file Coverage rating: A
...ages/workflow-executor/src/adapters/agent-client-agent-port.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/errors.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/index.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/executors/mcp-step-executor.ts98.3%144
New file Coverage rating: A
...ow-executor/src/executors/summary/step-execution-formatters.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/runner.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/types/step-outcome.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/stores/in-memory-store.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/stores/build-run-store.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/types/step-definition.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/schema-cache.ts100.0%
New file Coverage rating: A
packages/workflow-executor/src/stores/database-store.ts96.2%69
New file Coverage rating: A
packages/workflow-executor/src/validate-secrets.ts100.0%
Total98.7%
🤖 Increase coverage with AI coding...

In the `refactor/ai-proxy-align-mcp-client` branch, add test coverage for this new code:

- `packages/workflow-executor/src/build-workflow-executor.ts` -- Line 97-114
- `packages/workflow-executor/src/executors/load-related-record-step-executor.ts` -- Line 146
- `packages/workflow-executor/src/executors/mcp-step-executor.ts` -- Line 144
- `packages/workflow-executor/src/http/executor-http-server.ts` -- Line 119
- `packages/workflow-executor/src/stores/database-store.ts` -- Line 69

🚦 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.

…eability

Add sourceId to McpToolRef so that persisted execution data (pendingData,
executionParams) tracks which MCP server provided the tool. This fixes
tool lookup on re-entry (confirmation flow) when multiple servers expose
tools with the same name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread packages/workflow-executor/src/executors/mcp-step-executor.ts
matthv and others added 2 commits April 2, 2026 11:12
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… collection schema

- Replace non-null assertion with explicit McpToolNotFoundError when AI
  selects a tool name that doesn't match any available tool
- Resolve related collection name from parent schema before looking up
  the related schema in cache, fixing cases where relation name differs
  from target collection name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@matthv

matthv commented Apr 2, 2026

Copy link
Copy Markdown
Member Author

Macroscope review — triage

Reviewed all 7 Macroscope comments. Here's the disposition:

Fixed in this PR:

  • docs: add schema type documentation #5 resolveSchema(relation) (Medium) — getRelatedData was resolving the schema using the relation name instead of the target collection name. Now resolves via relatedCollectionName from the parent schema. (d43f168d)
  • fix: tests were not compiled #7 selectedTool!.sourceId (Medium) — Replaced non-null assertion with explicit McpToolNotFoundError guard. (d43f168d)

Not applicable / Ignored:

@matthv matthv closed this Apr 2, 2026
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