From 783118160834ee3a0716bff1790b173946875bc1 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 06:02:51 +0200 Subject: [PATCH 1/9] fix(agent): scope remote MCP tools to project --- .../hosted/child-fork-tool-sources.test.ts | 2 +- src/agent/hosted/child-fork-tool-sources.ts | 9 +++++++-- .../hosted/project-remote-tool-source.ts | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/agent/hosted/child-fork-tool-sources.test.ts b/src/agent/hosted/child-fork-tool-sources.test.ts index 0afb22bd41..4c61c19d10 100644 --- a/src/agent/hosted/child-fork-tool-sources.test.ts +++ b/src/agent/hosted/child-fork-tool-sources.test.ts @@ -165,7 +165,7 @@ Deno.test("prepareDefaultHostedChildForkToolSources loads API, live Studio, and assertEquals( fixtures.createdConfigs.map((config) => [config.id, config.endpoint]), [ - ["veryfront-mcp-fork", "https://api.example/mcp"], + ["veryfront-mcp-fork", "https://api.example/projects/project-1/mcp"], ["studio-mcp-live-tools", "https://studio.example/mcp"], ], ); diff --git a/src/agent/hosted/child-fork-tool-sources.ts b/src/agent/hosted/child-fork-tool-sources.ts index 30de2dc8b1..766ebaa6a5 100644 --- a/src/agent/hosted/child-fork-tool-sources.ts +++ b/src/agent/hosted/child-fork-tool-sources.ts @@ -31,7 +31,10 @@ import { } from "./child-requested-tools.ts"; import { createMcpToolPolicyGate, wrapHostToolSetWithMcpPolicy } from "../mcp-tool-policy.ts"; import { filterVeryfrontApiToolDefinitionsWithAccessProfile } from "./veryfront-api-tool-access.ts"; -import { createHostedMcpToolPolicySource } from "./project-remote-tool-source.ts"; +import { + createHostedMcpToolPolicySource, + createProjectScopedMcpUrl, +} from "./project-remote-tool-source.ts"; /** Public API contract for hosted child fork tool sources logger. */ export type HostedChildForkToolSourcesLogger = { @@ -129,7 +132,9 @@ export async function prepareDefaultHostedChildForkToolSources( const remoteConfig = createAgentServiceRemoteMcpConfig({ server, authToken: input.authToken, - apiMcpUrl: input.apiMcpUrl, + apiMcpUrl: server.kind === "veryfront-api" + ? createProjectScopedMcpUrl(input.apiMcpUrl, input.getProjectId()) + : input.apiMcpUrl, defaultSourceId: "veryfront-mcp-fork", }); if (!remoteConfig) { diff --git a/src/agent/hosted/project-remote-tool-source.ts b/src/agent/hosted/project-remote-tool-source.ts index 1760d842c4..48dc005775 100644 --- a/src/agent/hosted/project-remote-tool-source.ts +++ b/src/agent/hosted/project-remote-tool-source.ts @@ -473,7 +473,9 @@ export function createHostedProjectRemoteToolSources( const remoteConfig = createAgentServiceRemoteMcpConfig({ server, authToken: input.authToken, - apiMcpUrl: input.apiMcpUrl, + apiMcpUrl: server.kind === "veryfront-api" + ? createProjectScopedMcpUrl(input.apiMcpUrl, input.getProjectId()) + : input.apiMcpUrl, studioMcpUrl: input.studioMcpUrl, clientProfile: input.clientProfile, getProjectId: input.getProjectId, @@ -498,3 +500,18 @@ export function createHostedProjectRemoteToolSources( return sources; } + +export function createProjectScopedMcpUrl( + apiMcpUrl: string, + projectId: string | null | undefined, +): string { + if (!projectId) { + return apiMcpUrl; + } + + const url = new URL(apiMcpUrl); + url.pathname = `${url.pathname.replace(/\/mcp\/?$/, "")}/projects/${ + encodeURIComponent(projectId) + }/mcp`; + return url.toString(); +} From f7af726615f065bee7eb237f16e5b6afb08360a7 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 08:35:52 +0200 Subject: [PATCH 2/9] test(agent): cover project-scoped MCP endpoints --- docs/api-reference/veryfront/agent.md | 12 ++++---- .../hosted/project-remote-tool-source.test.ts | 29 +++++++++++++++---- .../veryfront-cloud-agent-service.test.ts | 4 +-- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 5119045b16..068ddd29c6 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -922,9 +922,9 @@ Input delivered to a hosted agent-service detached execution callback. | `prepareConversationRunExternalEvents` | Prepare conversation run external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L78) | | `prepareConversationRunStreamEvents` | Prepare conversation run stream events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L59) | | `prepareDefaultHostedChildForkRuntimeTools` | Prepare default hosted child fork runtime tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L312) | -| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L195) | +| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L199) | | `prepareDefaultHostedChildForkToolAssembly` | Prepare default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L366) | -| `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L87) | +| `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L89) | | `prepareHostedChatExecution` | Prepare hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L482) | | `prepareHostedChatRuntimeCreationOptions` | Options accepted by prepare hosted chat runtime creation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L361) | | `prepareHostedChatRuntimeMessages` | Prepare hosted chat runtime messages. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L613) | @@ -1365,7 +1365,7 @@ Input delivered to a hosted agent-service detached execution callback. | `DefaultHostedChildForkRuntimeToolPreparationResult` | Result returned from default hosted child fork runtime tool preparation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L169) | | `DefaultHostedChildForkToolAssemblyResult` | Result returned from default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L194) | | `DefaultHostedChildForkToolAssemblySourceResult` | Result returned from default hosted child fork tool assembly source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L181) | -| `DefaultHostedChildForkToolSourcesResult` | Result returned from default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L64) | +| `DefaultHostedChildForkToolSourcesResult` | Result returned from default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L66) | | `DefaultHostedInvokeAgentConfig` | Configuration used by default hosted invoke agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L99) | | `DefaultHostedInvokeAgentContext` | Context for default hosted invoke agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L85) | | `DefaultHostedInvokeAgentInput` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L246) | @@ -1515,7 +1515,7 @@ Input delivered to a hosted agent-service detached execution callback. | `HostedChildForkToolCallSnapshot` | Public API contract for hosted child fork tool call snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L41) | | `HostedChildForkToolInput` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-tool-input.ts#L76) | | `HostedChildForkToolResultSnapshot` | Public API contract for hosted child fork tool result snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L48) | -| `HostedChildForkToolSourcesLogger` | Public API contract for hosted child fork tool sources logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L37) | +| `HostedChildForkToolSourcesLogger` | Public API contract for hosted child fork tool sources logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L39) | | `HostedChildInvokeFailure` | Public API contract for hosted child invoke failure. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-invoke-tool.ts#L13) | | `HostedChildLifecycleAdapter` | Public API contract for hosted child lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-lifecycle.ts#L34) | | `HostedChildLifecycleRunnerOptions` | Options accepted by hosted child lifecycle runner. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-lifecycle.ts#L54) | @@ -1697,8 +1697,8 @@ Input delivered to a hosted agent-service detached execution callback. | `PreparedAgentServiceChatExecutionDetachedInput` | Input payload for prepared hosted chat execution detached. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L64) | | `PreparedAgentServiceChatExecutionRuntimeOptions` | Options accepted by prepared hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L34) | | `PreparedAgentServiceChatExecutionStreamInput` | Input payload for prepared hosted chat execution stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L56) | -| `PrepareDefaultHostedChildForkSandboxToolSourcesInput` | Input payload for prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L76) | -| `PrepareDefaultHostedChildForkToolSourcesInput` | Input payload for prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L42) | +| `PrepareDefaultHostedChildForkSandboxToolSourcesInput` | Input payload for prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L78) | +| `PrepareDefaultHostedChildForkToolSourcesInput` | Input payload for prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L44) | | `PreparedHostedChatExecution` | Public API contract for prepared hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L13) | | `PreparedHostedChatExecutionDetachedInput` | Input payload for prepared hosted chat execution detached. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L64) | | `PreparedHostedChatExecutionRuntimeOptions` | Options accepted by prepared hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L34) | diff --git a/src/agent/hosted/project-remote-tool-source.test.ts b/src/agent/hosted/project-remote-tool-source.test.ts index 9016ac7bee..92e4a50f41 100644 --- a/src/agent/hosted/project-remote-tool-source.test.ts +++ b/src/agent/hosted/project-remote-tool-source.test.ts @@ -14,6 +14,7 @@ import type { import { createHostedProjectRemoteToolSource, createHostedProjectRemoteToolSources, + createProjectScopedMcpUrl, } from "./project-remote-tool-source.ts"; import { NETWORK_ERROR, PERMISSION_DENIED, VeryfrontError } from "#veryfront/errors"; import { createUnconfirmedProjectContextSwitchResult } from "../project/context.ts"; @@ -88,6 +89,22 @@ function createRemoteSource(input: { }; } +describe("createProjectScopedMcpUrl", () => { + it("adds the project path without changing the origin or query", () => { + assertEquals( + createProjectScopedMcpUrl("https://api.example/mcp?environment=staging", "project/1"), + "https://api.example/projects/project%2F1/mcp?environment=staging", + ); + }); + + it("keeps the configured MCP URL when no project is active", () => { + assertEquals( + createProjectScopedMcpUrl("https://api.example/mcp", null), + "https://api.example/mcp", + ); + }); +}); + async function resolveTestHeaders( headers: RemoteMCPToolSourceConfig["headers"], context?: ToolExecutionContext, @@ -674,7 +691,7 @@ Deno.test("createHostedProjectRemoteToolSources defaults to first-party MCP serv assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp"]); assertEquals(configs.map((config) => config.endpoint), [ - "https://api.example/mcp", + "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", ]); }); @@ -791,7 +808,7 @@ Deno.test("createHostedProjectRemoteToolSources builds API and explicit gated St assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp"]); assertEquals(configs.map((config) => config.endpoint), [ - "https://api.example/mcp", + "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", ]); assertEquals(await resolveTestHeaders(configs[0]?.headers), { @@ -886,7 +903,7 @@ Deno.test("createHostedProjectRemoteToolSources infers Studio MCP from allowed S assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp"]); assertEquals(configs.map((config) => config.endpoint), [ - "https://api.example/mcp", + "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", ]); assertEquals( @@ -920,7 +937,9 @@ Deno.test("createHostedProjectRemoteToolSources does not infer Studio when expli }); assertEquals(sources.map((source) => source.id), ["veryfront-mcp"]); - assertEquals(configs.map((config) => config.endpoint), ["https://api.example/mcp"]); + assertEquals(configs.map((config) => config.endpoint), [ + "https://api.example/projects/project-1/mcp", + ]); }); Deno.test("createHostedProjectRemoteToolSources preserves an explicit MCP opt-out", () => { @@ -1028,7 +1047,7 @@ Deno.test("createHostedProjectRemoteToolSources builds explicit MCP server lists assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp", "linear"]); assertEquals(configs.map((config) => config.endpoint), [ - "https://api.example/mcp", + "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", "https://linear.example/mcp", ]); diff --git a/src/agent/hosted/veryfront-cloud-agent-service.test.ts b/src/agent/hosted/veryfront-cloud-agent-service.test.ts index b07a8378fc..a180cfe1a5 100644 --- a/src/agent/hosted/veryfront-cloud-agent-service.test.ts +++ b/src/agent/hosted/veryfront-cloud-agent-service.test.ts @@ -146,7 +146,7 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", assertEquals( createdConfigs.map(({ id, endpoint }) => ({ id, endpoint })), [ - { id: "veryfront-mcp", endpoint: "https://api.example/mcp" }, + { id: "veryfront-mcp", endpoint: "https://api.example/projects/project-1/mcp" }, { id: "studio-mcp", endpoint: "https://studio.example/mcp" }, ], ); @@ -168,7 +168,7 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", assertEquals( createdConfigs.map(({ id, endpoint }) => ({ id, endpoint })), [ - { id: "veryfront-mcp-fork", endpoint: "https://api.example/mcp" }, + { id: "veryfront-mcp-fork", endpoint: "https://api.example/projects/project-1/mcp" }, { id: "studio-mcp-live-tools", endpoint: "https://studio.example/mcp" }, ], ); From 8613be5d0845098eee128cfd275fe07c116fc532 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 08:43:37 +0200 Subject: [PATCH 3/9] fix(agent): preserve project scoped MCP child tools --- docs/api-reference/veryfront/agent.md | 12 +-- docs/api-reference/veryfront/tool.md | 6 +- .../hosted/child-fork-tool-sources.test.ts | 16 +++- src/agent/hosted/child-fork-tool-sources.ts | 10 +-- .../hosted/project-remote-tool-source.test.ts | 39 ++++----- .../hosted/project-remote-tool-source.ts | 19 +---- .../veryfront-cloud-agent-service.test.ts | 10 ++- .../runtime/mcp-server-tool-sources.test.ts | 17 +++- src/agent/service/mcp-server-config.test.ts | 35 +++++++- src/agent/service/mcp-server-config.ts | 21 ++++- src/tool/remote-mcp.test.ts | 60 ++++++++++++- src/tool/remote-mcp.ts | 85 ++++++++++++++----- 12 files changed, 239 insertions(+), 91 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 068ddd29c6..774ba85bd1 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -922,9 +922,9 @@ Input delivered to a hosted agent-service detached execution callback. | `prepareConversationRunExternalEvents` | Prepare conversation run external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L78) | | `prepareConversationRunStreamEvents` | Prepare conversation run stream events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L59) | | `prepareDefaultHostedChildForkRuntimeTools` | Prepare default hosted child fork runtime tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L312) | -| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L199) | +| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L195) | | `prepareDefaultHostedChildForkToolAssembly` | Prepare default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L366) | -| `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L89) | +| `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L86) | | `prepareHostedChatExecution` | Prepare hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L482) | | `prepareHostedChatRuntimeCreationOptions` | Options accepted by prepare hosted chat runtime creation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L361) | | `prepareHostedChatRuntimeMessages` | Prepare hosted chat runtime messages. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L613) | @@ -1365,7 +1365,7 @@ Input delivered to a hosted agent-service detached execution callback. | `DefaultHostedChildForkRuntimeToolPreparationResult` | Result returned from default hosted child fork runtime tool preparation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L169) | | `DefaultHostedChildForkToolAssemblyResult` | Result returned from default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L194) | | `DefaultHostedChildForkToolAssemblySourceResult` | Result returned from default hosted child fork tool assembly source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L181) | -| `DefaultHostedChildForkToolSourcesResult` | Result returned from default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L66) | +| `DefaultHostedChildForkToolSourcesResult` | Result returned from default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L63) | | `DefaultHostedInvokeAgentConfig` | Configuration used by default hosted invoke agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L99) | | `DefaultHostedInvokeAgentContext` | Context for default hosted invoke agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L85) | | `DefaultHostedInvokeAgentInput` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L246) | @@ -1515,7 +1515,7 @@ Input delivered to a hosted agent-service detached execution callback. | `HostedChildForkToolCallSnapshot` | Public API contract for hosted child fork tool call snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L41) | | `HostedChildForkToolInput` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-tool-input.ts#L76) | | `HostedChildForkToolResultSnapshot` | Public API contract for hosted child fork tool result snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L48) | -| `HostedChildForkToolSourcesLogger` | Public API contract for hosted child fork tool sources logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L39) | +| `HostedChildForkToolSourcesLogger` | Public API contract for hosted child fork tool sources logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L36) | | `HostedChildInvokeFailure` | Public API contract for hosted child invoke failure. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-invoke-tool.ts#L13) | | `HostedChildLifecycleAdapter` | Public API contract for hosted child lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-lifecycle.ts#L34) | | `HostedChildLifecycleRunnerOptions` | Options accepted by hosted child lifecycle runner. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-lifecycle.ts#L54) | @@ -1697,8 +1697,8 @@ Input delivered to a hosted agent-service detached execution callback. | `PreparedAgentServiceChatExecutionDetachedInput` | Input payload for prepared hosted chat execution detached. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L64) | | `PreparedAgentServiceChatExecutionRuntimeOptions` | Options accepted by prepared hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L34) | | `PreparedAgentServiceChatExecutionStreamInput` | Input payload for prepared hosted chat execution stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L56) | -| `PrepareDefaultHostedChildForkSandboxToolSourcesInput` | Input payload for prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L78) | -| `PrepareDefaultHostedChildForkToolSourcesInput` | Input payload for prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L44) | +| `PrepareDefaultHostedChildForkSandboxToolSourcesInput` | Input payload for prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L75) | +| `PrepareDefaultHostedChildForkToolSourcesInput` | Input payload for prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L41) | | `PreparedHostedChatExecution` | Public API contract for prepared hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L13) | | `PreparedHostedChatExecutionDetachedInput` | Input payload for prepared hosted chat execution detached. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L64) | | `PreparedHostedChatExecutionRuntimeOptions` | Options accepted by prepared hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L34) | diff --git a/docs/api-reference/veryfront/tool.md b/docs/api-reference/veryfront/tool.md index c644f32971..df3e198d3a 100644 --- a/docs/api-reference/veryfront/tool.md +++ b/docs/api-reference/veryfront/tool.md @@ -127,8 +127,8 @@ Create a typed tool definition. | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `createContext7ToolSource` | Create context7 tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/context7.ts#L28) | | `createProjectScopedRemoteToolCatalog` | Create project scoped remote tool catalog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L348) | -| `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L970) | -| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1002) | +| `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L983) | +| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1014) | | `createSleepTool` | Create sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L54) | | `createToolsFromHostDefinitions` | Create tools from host definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/host-tools.ts#L96) | | `createToolsFromRemoteDefinitions` | Create tools from remote definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L29) | @@ -170,7 +170,7 @@ Create a typed tool definition. | `ProjectScopedRemoteToolExecutionInput` | Input payload for project scoped remote tool execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L38) | | `ProjectScopedRemoteToolOptions` | Options accepted by project scoped remote tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L6) | | `RemoteMCPToolSourceConfig` | Configuration used by remote MCP tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L52) | -| `RemoteMCPToolSourceTransportOptions` | Deployment-owned transport policy for exact, immutable MCP endpoints. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L977) | +| `RemoteMCPToolSourceTransportOptions` | Deployment-owned transport policy for trusted MCP endpoint roots. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L990) | | `RemoteToolMaterializationOptions` | Options accepted by remote tool materialization. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L8) | | `RemoteToolSource` | Remote tool source loaded dynamically at runtime. Hosts can provide these to expose tools from remote MCP-compatible systems without registering those tools globally inside the framework. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/types.ts#L231) | | `SleepToolInput` | Input payload for sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L45) | diff --git a/src/agent/hosted/child-fork-tool-sources.test.ts b/src/agent/hosted/child-fork-tool-sources.test.ts index 4c61c19d10..3f635178c0 100644 --- a/src/agent/hosted/child-fork-tool-sources.test.ts +++ b/src/agent/hosted/child-fork-tool-sources.test.ts @@ -132,6 +132,7 @@ Deno.test("prepareDefaultHostedChildForkToolSources loads API, live Studio, and const fixtures = createRemoteSourceFixtures(); const switchedProjectIds: string[] = []; const switchedProjects: Array<{ projectId: string; projectSlug?: string }> = []; + let activeProjectId = "project-1"; const result = await prepareDefaultHostedChildForkToolSources({ authToken: "token-1", @@ -139,7 +140,7 @@ Deno.test("prepareDefaultHostedChildForkToolSources loads API, live Studio, and mcpServers: [{ kind: "veryfront-api" }, { kind: "veryfront-studio" }], studioMcpUrl: "https://studio.example/mcp", clientProfile: trustedStudioProfile, - getProjectId: () => "project-1", + getProjectId: () => activeProjectId, conversationId: "conversation-1", globalTools: { sleep: { @@ -148,6 +149,7 @@ Deno.test("prepareDefaultHostedChildForkToolSources loads API, live Studio, and }, }, onConfirmedStudioProjectSwitch: (projectId, confirmedProject) => { + activeProjectId = projectId; switchedProjectIds.push(projectId); if (confirmedProject) { switchedProjects.push(confirmedProject); @@ -163,7 +165,12 @@ Deno.test("prepareDefaultHostedChildForkToolSources loads API, live Studio, and assertEquals(Object.keys(result.forkTools), ["sleep", "studio_open_project", "update_file"]); assertEquals( - fixtures.createdConfigs.map((config) => [config.id, config.endpoint]), + await Promise.all( + fixtures.createdConfigs.map(async (config) => [ + config.id, + typeof config.endpoint === "function" ? await config.endpoint() : config.endpoint, + ]), + ), [ ["veryfront-mcp-fork", "https://api.example/projects/project-1/mcp"], ["studio-mcp-live-tools", "https://studio.example/mcp"], @@ -174,6 +181,11 @@ Deno.test("prepareDefaultHostedChildForkToolSources loads API, live Studio, and assertEquals(switchedProjectIds, ["project-2"]); assertEquals(switchedProjects, [{ projectId: "project-2", projectSlug: "project-two" }]); + const apiConfig = fixtures.createdConfigs.find((config) => config.id === "veryfront-mcp-fork"); + assertEquals( + typeof apiConfig?.endpoint === "function" ? await apiConfig.endpoint() : apiConfig?.endpoint, + "https://api.example/projects/project-2/mcp", + ); assertEquals(fixtures.executeCalls, [ { sourceId: "veryfront-mcp-fork", diff --git a/src/agent/hosted/child-fork-tool-sources.ts b/src/agent/hosted/child-fork-tool-sources.ts index 766ebaa6a5..1377dac057 100644 --- a/src/agent/hosted/child-fork-tool-sources.ts +++ b/src/agent/hosted/child-fork-tool-sources.ts @@ -31,10 +31,7 @@ import { } from "./child-requested-tools.ts"; import { createMcpToolPolicyGate, wrapHostToolSetWithMcpPolicy } from "../mcp-tool-policy.ts"; import { filterVeryfrontApiToolDefinitionsWithAccessProfile } from "./veryfront-api-tool-access.ts"; -import { - createHostedMcpToolPolicySource, - createProjectScopedMcpUrl, -} from "./project-remote-tool-source.ts"; +import { createHostedMcpToolPolicySource } from "./project-remote-tool-source.ts"; /** Public API contract for hosted child fork tool sources logger. */ export type HostedChildForkToolSourcesLogger = { @@ -132,9 +129,8 @@ export async function prepareDefaultHostedChildForkToolSources( const remoteConfig = createAgentServiceRemoteMcpConfig({ server, authToken: input.authToken, - apiMcpUrl: server.kind === "veryfront-api" - ? createProjectScopedMcpUrl(input.apiMcpUrl, input.getProjectId()) - : input.apiMcpUrl, + apiMcpUrl: input.apiMcpUrl, + getProjectId: input.getProjectId, defaultSourceId: "veryfront-mcp-fork", }); if (!remoteConfig) { diff --git a/src/agent/hosted/project-remote-tool-source.test.ts b/src/agent/hosted/project-remote-tool-source.test.ts index 92e4a50f41..21e96116f3 100644 --- a/src/agent/hosted/project-remote-tool-source.test.ts +++ b/src/agent/hosted/project-remote-tool-source.test.ts @@ -14,7 +14,6 @@ import type { import { createHostedProjectRemoteToolSource, createHostedProjectRemoteToolSources, - createProjectScopedMcpUrl, } from "./project-remote-tool-source.ts"; import { NETWORK_ERROR, PERMISSION_DENIED, VeryfrontError } from "#veryfront/errors"; import { createUnconfirmedProjectContextSwitchResult } from "../project/context.ts"; @@ -89,22 +88,6 @@ function createRemoteSource(input: { }; } -describe("createProjectScopedMcpUrl", () => { - it("adds the project path without changing the origin or query", () => { - assertEquals( - createProjectScopedMcpUrl("https://api.example/mcp?environment=staging", "project/1"), - "https://api.example/projects/project%2F1/mcp?environment=staging", - ); - }); - - it("keeps the configured MCP URL when no project is active", () => { - assertEquals( - createProjectScopedMcpUrl("https://api.example/mcp", null), - "https://api.example/mcp", - ); - }); -}); - async function resolveTestHeaders( headers: RemoteMCPToolSourceConfig["headers"], context?: ToolExecutionContext, @@ -112,6 +95,12 @@ async function resolveTestHeaders( return typeof headers === "function" ? await headers(context) : headers; } +async function resolveTestEndpoint( + endpoint: RemoteMCPToolSourceConfig["endpoint"], +): Promise { + return typeof endpoint === "function" ? await endpoint() : endpoint; +} + Deno.test("hosted remote tool sources do not carry legacy end-user identity plumbing", async () => { const forbidden = [ ["get", "End", "User", "Id"].join(""), @@ -690,7 +679,7 @@ Deno.test("createHostedProjectRemoteToolSources defaults to first-party MCP serv }); assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp"]); - assertEquals(configs.map((config) => config.endpoint), [ + assertEquals(await Promise.all(configs.map((config) => resolveTestEndpoint(config.endpoint))), [ "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", ]); @@ -807,7 +796,7 @@ Deno.test("createHostedProjectRemoteToolSources builds API and explicit gated St }); assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp"]); - assertEquals(configs.map((config) => config.endpoint), [ + assertEquals(await Promise.all(configs.map((config) => resolveTestEndpoint(config.endpoint))), [ "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", ]); @@ -819,6 +808,10 @@ Deno.test("createHostedProjectRemoteToolSources builds API and explicit gated St }); activeProjectId = "project-2"; + assertEquals( + await resolveTestEndpoint(configs[0]?.endpoint ?? ""), + "https://api.example/projects/project-2/mcp", + ); assertEquals(await resolveTestHeaders(configs[1]?.headers), { Authorization: "Bearer token-1", "x-conversation-id": "conversation-1", @@ -902,7 +895,7 @@ Deno.test("createHostedProjectRemoteToolSources infers Studio MCP from allowed S }); assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp"]); - assertEquals(configs.map((config) => config.endpoint), [ + assertEquals(await Promise.all(configs.map((config) => resolveTestEndpoint(config.endpoint))), [ "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", ]); @@ -912,7 +905,7 @@ Deno.test("createHostedProjectRemoteToolSources infers Studio MCP from allowed S ); }); -Deno.test("createHostedProjectRemoteToolSources does not infer Studio when explicit API-only MCP is set", () => { +Deno.test("createHostedProjectRemoteToolSources does not infer Studio when explicit API-only MCP is set", async () => { const configs: RemoteMCPToolSourceConfig[] = []; const sources = createHostedProjectRemoteToolSources({ authToken: "token-1", @@ -937,7 +930,7 @@ Deno.test("createHostedProjectRemoteToolSources does not infer Studio when expli }); assertEquals(sources.map((source) => source.id), ["veryfront-mcp"]); - assertEquals(configs.map((config) => config.endpoint), [ + assertEquals(await Promise.all(configs.map((config) => resolveTestEndpoint(config.endpoint))), [ "https://api.example/projects/project-1/mcp", ]); }); @@ -1046,7 +1039,7 @@ Deno.test("createHostedProjectRemoteToolSources builds explicit MCP server lists }); assertEquals(sources.map((source) => source.id), ["veryfront-mcp", "studio-mcp", "linear"]); - assertEquals(configs.map((config) => config.endpoint), [ + assertEquals(await Promise.all(configs.map((config) => resolveTestEndpoint(config.endpoint))), [ "https://api.example/projects/project-1/mcp", "https://studio.example/mcp", "https://linear.example/mcp", diff --git a/src/agent/hosted/project-remote-tool-source.ts b/src/agent/hosted/project-remote-tool-source.ts index 48dc005775..1760d842c4 100644 --- a/src/agent/hosted/project-remote-tool-source.ts +++ b/src/agent/hosted/project-remote-tool-source.ts @@ -473,9 +473,7 @@ export function createHostedProjectRemoteToolSources( const remoteConfig = createAgentServiceRemoteMcpConfig({ server, authToken: input.authToken, - apiMcpUrl: server.kind === "veryfront-api" - ? createProjectScopedMcpUrl(input.apiMcpUrl, input.getProjectId()) - : input.apiMcpUrl, + apiMcpUrl: input.apiMcpUrl, studioMcpUrl: input.studioMcpUrl, clientProfile: input.clientProfile, getProjectId: input.getProjectId, @@ -500,18 +498,3 @@ export function createHostedProjectRemoteToolSources( return sources; } - -export function createProjectScopedMcpUrl( - apiMcpUrl: string, - projectId: string | null | undefined, -): string { - if (!projectId) { - return apiMcpUrl; - } - - const url = new URL(apiMcpUrl); - url.pathname = `${url.pathname.replace(/\/mcp\/?$/, "")}/projects/${ - encodeURIComponent(projectId) - }/mcp`; - return url.toString(); -} diff --git a/src/agent/hosted/veryfront-cloud-agent-service.test.ts b/src/agent/hosted/veryfront-cloud-agent-service.test.ts index a180cfe1a5..bbc61025c4 100644 --- a/src/agent/hosted/veryfront-cloud-agent-service.test.ts +++ b/src/agent/hosted/veryfront-cloud-agent-service.test.ts @@ -144,7 +144,10 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", clientProfile, }); assertEquals( - createdConfigs.map(({ id, endpoint }) => ({ id, endpoint })), + await Promise.all(createdConfigs.map(async ({ id, endpoint }) => ({ + id, + endpoint: typeof endpoint === "function" ? await endpoint() : endpoint, + }))), [ { id: "veryfront-mcp", endpoint: "https://api.example/projects/project-1/mcp" }, { id: "studio-mcp", endpoint: "https://studio.example/mcp" }, @@ -166,7 +169,10 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", prompt: "Inspect the available tools.", }, { toolCallId: "tool-call-1" }); assertEquals( - createdConfigs.map(({ id, endpoint }) => ({ id, endpoint })), + await Promise.all(createdConfigs.map(async ({ id, endpoint }) => ({ + id, + endpoint: typeof endpoint === "function" ? await endpoint() : endpoint, + }))), [ { id: "veryfront-mcp-fork", endpoint: "https://api.example/projects/project-1/mcp" }, { id: "studio-mcp-live-tools", endpoint: "https://studio.example/mcp" }, diff --git a/src/agent/runtime/mcp-server-tool-sources.test.ts b/src/agent/runtime/mcp-server-tool-sources.test.ts index 8b24fbc52f..243c0122cf 100644 --- a/src/agent/runtime/mcp-server-tool-sources.test.ts +++ b/src/agent/runtime/mcp-server-tool-sources.test.ts @@ -32,6 +32,13 @@ type FetchCall = { init: RequestInit; }; +async function resolveRemoteEndpoint( + endpoint: RemoteMCPToolSourceConfig["endpoint"] | undefined, +): Promise { + if (endpoint === undefined) return undefined; + return typeof endpoint === "function" ? await endpoint() : endpoint; +} + function createMcpFetch(calls: FetchCall[]): typeof fetch { return ((url: string | URL | Request, init?: RequestInit) => { calls.push({ url: String(url), init: init ?? {} }); @@ -170,7 +177,10 @@ Deno.test("getRuntimeRemoteToolSources hydrates a Veryfront API MCP server from ); assertEquals(sources?.length, 1); - assertEquals(remoteConfig?.endpoint, "https://api.example/mcp"); + assertEquals( + await resolveRemoteEndpoint(remoteConfig?.endpoint), + "https://api.example/projects/server-project/mcp", + ); assertEquals( await (remoteConfig?.headers as (context?: ToolExecutionContext) => HeadersInit)?.({ authToken: "browser-token", @@ -322,7 +332,10 @@ Deno.test("getRuntimeRemoteToolSources implicitly connects unresolved named tool }, ); - assertEquals(remoteConfig?.endpoint, "https://api.example/mcp"); + assertEquals( + await resolveRemoteEndpoint(remoteConfig?.endpoint), + "https://api.example/projects/server-project/mcp", + ); assertEquals((await sources?.[0]?.listTools())?.map((tool) => tool.name), ["get_file"]); }); diff --git a/src/agent/service/mcp-server-config.test.ts b/src/agent/service/mcp-server-config.test.ts index f1b2e74d60..8a811935e5 100644 --- a/src/agent/service/mcp-server-config.test.ts +++ b/src/agent/service/mcp-server-config.test.ts @@ -1,6 +1,7 @@ import { assertEquals } from "#veryfront/testing/assert.ts"; import { createAgentServiceRemoteMcpConfig, + createProjectScopedMcpUrl, defaultAgentServiceMcpServers, } from "./mcp-server-config.ts"; @@ -12,13 +13,23 @@ Deno.test("defaultAgentServiceMcpServers enables first-party MCP servers", () => }); Deno.test("createAgentServiceRemoteMcpConfig builds Veryfront API MCP config", async () => { + let projectId = "project-1"; const config = createAgentServiceRemoteMcpConfig({ server: { kind: "veryfront-api" }, authToken: "token-1", apiMcpUrl: "https://api.example/mcp", + getProjectId: () => projectId, }); assertEquals(config?.id, "veryfront-mcp"); - assertEquals(config?.endpoint, "https://api.example/mcp"); + assertEquals( + typeof config?.endpoint === "function" ? await config.endpoint() : config?.endpoint, + "https://api.example/projects/project-1/mcp", + ); + projectId = "project-2"; + assertEquals( + typeof config?.endpoint === "function" ? await config.endpoint() : config?.endpoint, + "https://api.example/projects/project-2/mcp", + ); assertEquals( typeof config?.headers === "function" ? await config.headers() : config?.headers, { @@ -45,6 +56,28 @@ Deno.test("createAgentServiceRemoteMcpConfig builds Veryfront API MCP config", a ); }); +Deno.test("createProjectScopedMcpUrl normalizes and replaces the project segment", () => { + assertEquals( + createProjectScopedMcpUrl("https://api.example", " project/1 "), + "https://api.example/projects/project%2F1/mcp", + ); + assertEquals( + createProjectScopedMcpUrl("https://api.example/projects/old/mcp", "new"), + "https://api.example/projects/new/mcp", + ); + assertEquals( + createProjectScopedMcpUrl("https://api.example/mcp", " "), + "https://api.example/mcp", + ); + assertEquals( + createProjectScopedMcpUrl( + "https://api.example/mcp/?environment=staging", + "project-1", + ), + "https://api.example/projects/project-1/mcp?environment=staging", + ); +}); + Deno.test("createAgentServiceRemoteMcpConfig builds generic MCP config without dropping options", () => { const headers = { Authorization: "Bearer external-token" }; assertEquals( diff --git a/src/agent/service/mcp-server-config.ts b/src/agent/service/mcp-server-config.ts index 6ba44f8dc6..158571800b 100644 --- a/src/agent/service/mcp-server-config.ts +++ b/src/agent/service/mcp-server-config.ts @@ -45,6 +45,23 @@ export function defaultAgentServiceMcpServers(): AgentServiceMcpServerConfig[] { return [{ kind: "veryfront-api" }, { kind: "veryfront-studio" }]; } +/** Build the project-scoped control-plane MCP URL for the active project. */ +export function createProjectScopedMcpUrl( + apiMcpUrl: string, + projectId: string | null | undefined, +): string { + const normalizedProjectId = projectId?.trim(); + if (!normalizedProjectId) return apiMcpUrl; + + const url = new URL(apiMcpUrl); + const basePath = url.pathname + .replace(/\/projects\/[^/]+\/mcp\/?$/, "") + .replace(/\/mcp\/?$/, "") + .replace(/\/+$/, ""); + url.pathname = `${basePath}/projects/${encodeURIComponent(normalizedProjectId)}/mcp`; + return url.toString(); +} + function createGenericRemoteMcpConfig( server: AgentServiceGenericMcpServerConfig, ): RemoteMCPToolSourceConfig { @@ -63,13 +80,13 @@ function createGenericRemoteMcpConfig( function createVeryfrontApiRemoteMcpConfig( input: Pick< CreateAgentServiceRemoteMcpConfigInput, - "apiMcpUrl" | "authToken" | "defaultSourceId" + "apiMcpUrl" | "authToken" | "defaultSourceId" | "getProjectId" >, server: AgentServiceVeryfrontApiMcpServerConfig, ): RemoteMCPToolSourceConfig { return { id: server.id ?? input.defaultSourceId ?? "veryfront-mcp", - endpoint: input.apiMcpUrl, + endpoint: () => createProjectScopedMcpUrl(input.apiMcpUrl, input.getProjectId?.()), headers: (context) => { const authToken = typeof context?.authToken === "string" && context.authToken.length > 0 ? context.authToken diff --git a/src/tool/remote-mcp.test.ts b/src/tool/remote-mcp.test.ts index 443efbbca2..4b1a05754b 100644 --- a/src/tool/remote-mcp.test.ts +++ b/src/tool/remote-mcp.test.ts @@ -87,7 +87,7 @@ describe("tool/remote-mcp", () => { assertEquals(transportCalls, 0); }); - it("keeps unmatched and dynamic endpoints on guarded transport", async () => { + it("keeps unmatched endpoints on guarded transport", async () => { let transportCalls = 0; const createSource = createRemoteMCPToolSourceFactoryWithTransport({ trustedEndpoints: ["http://veryfront-api/mcp"], @@ -100,7 +100,7 @@ describe("tool/remote-mcp", () => { for ( const endpoint of [ "http://169.254.169.254/latest/meta-data", - () => "http://veryfront-api/mcp", + () => "http://169.254.169.254/latest/meta-data", ] ) { const source = createSource({ id: "untrusted", endpoint }); @@ -113,6 +113,29 @@ describe("tool/remote-mcp", () => { assertEquals(transportCalls, 0); }); + it("uses host transport for a dynamic project-scoped trusted endpoint", async () => { + let transportCalls = 0; + const createSource = createRemoteMCPToolSourceFactoryWithTransport({ + trustedEndpoints: ["http://veryfront-api/mcp"], + requestFetch: async (_input, init) => { + transportCalls++; + const body = JSON.parse(String(init && "body" in init ? init.body : undefined)) as { + id: string; + }; + return Response.json({ jsonrpc: "2.0", id: body.id, result: { tools: [] } }); + }, + }); + let projectId = "project-1"; + const source = createSource({ + endpoint: () => `http://veryfront-api/projects/${projectId}/mcp`, + }); + + assertEquals(await source.listTools(), []); + projectId = "project-2"; + assertEquals(await source.listTools(), []); + assertEquals(transportCalls, 2); + }); + it("rejects invalid trusted endpoints when the factory is created", () => { const error = assertThrows( () => @@ -334,6 +357,39 @@ describe("tool/remote-mcp", () => { }); }); + it("omits non-binding run ids from project-scoped control-plane MCP metadata", async () => { + let requestBody: Record | undefined; + await withEnv({ VERYFRONT_API_BASE_URL: "https://93.184.216.34" }, async () => { + const source = createRemoteMCPToolSource({ + id: "veryfront-mcp", + endpoint: "https://93.184.216.34/projects/project-1/mcp", + }); + + await withMockFetch( + async (input: string | URL | Request, init?: RequestInit) => { + const request = input instanceof Request ? input : new Request(input, init); + requestBody = await request.json(); + return Response.json({ + jsonrpc: "2.0", + id: "veryfront-mcp:tools:call:gmail__get_profile", + result: { content: [], structuredContent: { ok: true } }, + }); + }, + async () => + await source.executeTool("gmail__get_profile", {}, { + runId: "run-local", + runIdBindsToolAuthorization: false, + agentId: "gmail-agent", + }), + ); + + assertEquals( + (requestBody as { params?: { _meta?: Record } }).params?._meta, + { agent_id: "gmail-agent" }, + ); + }); + }); + it("keeps run ids for same-origin MCP servers outside the control-plane path", async () => { let requestBody: Record | undefined; await withEnv({ VERYFRONT_API_BASE_URL: "https://93.184.216.34" }, async () => { diff --git a/src/tool/remote-mcp.ts b/src/tool/remote-mcp.ts index 40ce252de4..58e2d57b5a 100644 --- a/src/tool/remote-mcp.ts +++ b/src/tool/remote-mcp.ts @@ -814,10 +814,24 @@ function endpointBindsToolAuthorization(endpoint: string): boolean { const apiBaseUrl = getApiBaseUrlEnv(); if (typeof apiBaseUrl !== "string" || apiBaseUrl.length === 0) return false; const normalizedEndpoint = normalizeTrustedEndpoint(endpoint); - const controlPlaneEndpoint = normalizeTrustedEndpoint( - `${apiBaseUrl.replace(/\/+$/, "")}/mcp`, - ); - return normalizedEndpoint !== undefined && normalizedEndpoint === controlPlaneEndpoint; + if (!normalizedEndpoint) return false; + + try { + const endpointUrl = new URL(normalizedEndpoint); + const apiBase = new URL(apiBaseUrl); + if (endpointUrl.origin !== apiBase.origin) return false; + + const basePath = apiBase.pathname.replace(/\/+$/, ""); + const controlPlanePath = `${basePath}/mcp`; + if (endpointUrl.pathname === controlPlanePath) return true; + + const projectPathPrefix = `${basePath}/projects/`; + if (!endpointUrl.pathname.startsWith(projectPathPrefix)) return false; + const projectScopedPath = endpointUrl.pathname.slice(projectPathPrefix.length); + return /^[^/]+\/mcp\/?$/.test(projectScopedPath); + } catch { + return false; + } } function buildRunContextMeta( @@ -843,7 +857,7 @@ function buildRunContextMeta( function createRemoteMCPToolSourceWithFetch( config: RemoteMCPToolSourceConfig, - requestFetch: typeof fetch, + getRequestFetch: (endpoint: string) => typeof fetch, ): RemoteToolSource { const id = config.id ?? "remote-mcp"; const listMethod = config.listMethod ?? "tools/list"; @@ -870,7 +884,7 @@ function createRemoteMCPToolSourceWithFetch( method: listMethod, ...(cursor !== undefined ? { params: { cursor } } : {}), }, - requestFetch, + getRequestFetch(endpoint), context?.abortSignal, MAX_REMOTE_MCP_TOOL_LIST_RESPONSE_BYTES, ); @@ -938,7 +952,7 @@ function createRemoteMCPToolSourceWithFetch( ...(meta ? { _meta: meta } : {}), }, }, - requestFetch, + getRequestFetch(endpoint), context?.abortSignal, MAX_REMOTE_MCP_CALL_RESPONSE_BYTES, ); @@ -970,10 +984,10 @@ function createRemoteMCPToolSourceWithFetch( export function createRemoteMCPToolSource( config: RemoteMCPToolSourceConfig, ): RemoteToolSource { - return createRemoteMCPToolSourceWithFetch(config, guardedOutboundFetch); + return createRemoteMCPToolSourceWithFetch(config, () => guardedOutboundFetch); } -/** Deployment-owned transport policy for exact, immutable MCP endpoints. */ +/** Deployment-owned transport policy for trusted MCP endpoint roots. */ export interface RemoteMCPToolSourceTransportOptions { /** Complete endpoint URLs allowed to use {@link requestFetch}. */ trustedEndpoints: readonly string[]; @@ -995,9 +1009,8 @@ function normalizeTrustedEndpoint(value: string): string | undefined { /** * Create a remote MCP source factory with narrowly scoped host transport. * - * Only static endpoint strings that exactly match a normalized deployment - * allowlist use the supplied transport. Invalid, unmatched, or resolver-based - * endpoints retain {@link createRemoteMCPToolSource}'s guarded outbound path. + * Exact trusted endpoints and their project-scoped MCP routes use the supplied + * transport. All other resolved endpoints retain the guarded outbound path. */ export function createRemoteMCPToolSourceFactoryWithTransport( options: RemoteMCPToolSourceTransportOptions, @@ -1011,16 +1024,42 @@ export function createRemoteMCPToolSourceFactoryWithTransport( trustedEndpoints.add(endpoint); } - return (config) => { - const endpoint = typeof config.endpoint === "string" - ? normalizeTrustedEndpoint(config.endpoint) - : undefined; - if (!endpoint || !trustedEndpoints.has(endpoint)) { - return createRemoteMCPToolSource(config); - } - return createRemoteMCPToolSourceWithFetch( - { ...config, endpoint }, - options.requestFetch, + return (config) => + createRemoteMCPToolSourceWithFetch( + config, + (endpoint) => + isTrustedDeploymentMcpEndpoint(endpoint, trustedEndpoints) + ? options.requestFetch + : guardedOutboundFetch, ); - }; +} + +function isTrustedDeploymentMcpEndpoint( + endpoint: string, + trustedEndpoints: ReadonlySet, +): boolean { + const normalizedEndpoint = normalizeTrustedEndpoint(endpoint); + if (!normalizedEndpoint) return false; + if (trustedEndpoints.has(normalizedEndpoint)) return true; + + let endpointUrl: URL; + try { + endpointUrl = new URL(normalizedEndpoint); + } catch { + return false; + } + + for (const trustedEndpoint of trustedEndpoints) { + const trustedUrl = new URL(trustedEndpoint); + if (endpointUrl.origin !== trustedUrl.origin) continue; + + const trustedPath = trustedUrl.pathname.replace(/\/+$/, ""); + if (!trustedPath.endsWith("/mcp")) continue; + const projectPathPrefix = `${trustedPath.slice(0, -4)}/projects/`; + if (!endpointUrl.pathname.startsWith(projectPathPrefix)) continue; + const projectScopedPath = endpointUrl.pathname.slice(projectPathPrefix.length); + if (/^[^/]+\/mcp\/?$/.test(projectScopedPath)) return true; + } + + return false; } From f9ea9163856b64373ac37d17e4edfb09384e0403 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 08:50:38 +0200 Subject: [PATCH 4/9] docs: refresh MCP API references --- docs/api-reference/veryfront/agent.md | 12 ++++++------ docs/api-reference/veryfront/tool.md | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 774ba85bd1..6d5251061e 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -922,9 +922,9 @@ Input delivered to a hosted agent-service detached execution callback. | `prepareConversationRunExternalEvents` | Prepare conversation run external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L78) | | `prepareConversationRunStreamEvents` | Prepare conversation run stream events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L59) | | `prepareDefaultHostedChildForkRuntimeTools` | Prepare default hosted child fork runtime tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L312) | -| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L195) | +| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L196) | | `prepareDefaultHostedChildForkToolAssembly` | Prepare default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L366) | -| `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L86) | +| `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L87) | | `prepareHostedChatExecution` | Prepare hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L482) | | `prepareHostedChatRuntimeCreationOptions` | Options accepted by prepare hosted chat runtime creation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L361) | | `prepareHostedChatRuntimeMessages` | Prepare hosted chat runtime messages. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L613) | @@ -1365,7 +1365,7 @@ Input delivered to a hosted agent-service detached execution callback. | `DefaultHostedChildForkRuntimeToolPreparationResult` | Result returned from default hosted child fork runtime tool preparation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L169) | | `DefaultHostedChildForkToolAssemblyResult` | Result returned from default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L194) | | `DefaultHostedChildForkToolAssemblySourceResult` | Result returned from default hosted child fork tool assembly source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L181) | -| `DefaultHostedChildForkToolSourcesResult` | Result returned from default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L63) | +| `DefaultHostedChildForkToolSourcesResult` | Result returned from default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L64) | | `DefaultHostedInvokeAgentConfig` | Configuration used by default hosted invoke agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L99) | | `DefaultHostedInvokeAgentContext` | Context for default hosted invoke agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L85) | | `DefaultHostedInvokeAgentInput` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L246) | @@ -1515,7 +1515,7 @@ Input delivered to a hosted agent-service detached execution callback. | `HostedChildForkToolCallSnapshot` | Public API contract for hosted child fork tool call snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L41) | | `HostedChildForkToolInput` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-tool-input.ts#L76) | | `HostedChildForkToolResultSnapshot` | Public API contract for hosted child fork tool result snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L48) | -| `HostedChildForkToolSourcesLogger` | Public API contract for hosted child fork tool sources logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L36) | +| `HostedChildForkToolSourcesLogger` | Public API contract for hosted child fork tool sources logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L37) | | `HostedChildInvokeFailure` | Public API contract for hosted child invoke failure. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-invoke-tool.ts#L13) | | `HostedChildLifecycleAdapter` | Public API contract for hosted child lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-lifecycle.ts#L34) | | `HostedChildLifecycleRunnerOptions` | Options accepted by hosted child lifecycle runner. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-lifecycle.ts#L54) | @@ -1697,8 +1697,8 @@ Input delivered to a hosted agent-service detached execution callback. | `PreparedAgentServiceChatExecutionDetachedInput` | Input payload for prepared hosted chat execution detached. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L64) | | `PreparedAgentServiceChatExecutionRuntimeOptions` | Options accepted by prepared hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L34) | | `PreparedAgentServiceChatExecutionStreamInput` | Input payload for prepared hosted chat execution stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L56) | -| `PrepareDefaultHostedChildForkSandboxToolSourcesInput` | Input payload for prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L75) | -| `PrepareDefaultHostedChildForkToolSourcesInput` | Input payload for prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L41) | +| `PrepareDefaultHostedChildForkSandboxToolSourcesInput` | Input payload for prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L76) | +| `PrepareDefaultHostedChildForkToolSourcesInput` | Input payload for prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L42) | | `PreparedHostedChatExecution` | Public API contract for prepared hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L13) | | `PreparedHostedChatExecutionDetachedInput` | Input payload for prepared hosted chat execution detached. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L64) | | `PreparedHostedChatExecutionRuntimeOptions` | Options accepted by prepared hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/prepared-chat-execution.ts#L34) | diff --git a/docs/api-reference/veryfront/tool.md b/docs/api-reference/veryfront/tool.md index df3e198d3a..94961725af 100644 --- a/docs/api-reference/veryfront/tool.md +++ b/docs/api-reference/veryfront/tool.md @@ -127,8 +127,8 @@ Create a typed tool definition. | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `createContext7ToolSource` | Create context7 tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/context7.ts#L28) | | `createProjectScopedRemoteToolCatalog` | Create project scoped remote tool catalog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L348) | -| `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L983) | -| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1014) | +| `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L984) | +| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1015) | | `createSleepTool` | Create sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L54) | | `createToolsFromHostDefinitions` | Create tools from host definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/host-tools.ts#L96) | | `createToolsFromRemoteDefinitions` | Create tools from remote definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L29) | @@ -170,7 +170,7 @@ Create a typed tool definition. | `ProjectScopedRemoteToolExecutionInput` | Input payload for project scoped remote tool execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L38) | | `ProjectScopedRemoteToolOptions` | Options accepted by project scoped remote tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L6) | | `RemoteMCPToolSourceConfig` | Configuration used by remote MCP tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L52) | -| `RemoteMCPToolSourceTransportOptions` | Deployment-owned transport policy for trusted MCP endpoint roots. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L990) | +| `RemoteMCPToolSourceTransportOptions` | Deployment-owned transport policy for trusted MCP endpoint roots. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L991) | | `RemoteToolMaterializationOptions` | Options accepted by remote tool materialization. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L8) | | `RemoteToolSource` | Remote tool source loaded dynamically at runtime. Hosts can provide these to expose tools from remote MCP-compatible systems without registering those tools globally inside the framework. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/types.ts#L231) | | `SleepToolInput` | Input payload for sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L45) | From fc2dcebcfff3505b9a94762541baae1fcd6576fb Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 08:52:48 +0200 Subject: [PATCH 5/9] fix(agent): preserve query scoped MCP trust --- docs/api-reference/veryfront/tool.md | 6 +-- .../hosted/project-remote-tool-source.test.ts | 2 +- src/agent/service/mcp-server-config.test.ts | 43 ++++++++++--------- src/tool/remote-mcp.test.ts | 14 ++++-- src/tool/remote-mcp.ts | 26 +++++++++-- 5 files changed, 59 insertions(+), 32 deletions(-) diff --git a/docs/api-reference/veryfront/tool.md b/docs/api-reference/veryfront/tool.md index 94961725af..e416c5b83f 100644 --- a/docs/api-reference/veryfront/tool.md +++ b/docs/api-reference/veryfront/tool.md @@ -127,8 +127,8 @@ Create a typed tool definition. | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `createContext7ToolSource` | Create context7 tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/context7.ts#L28) | | `createProjectScopedRemoteToolCatalog` | Create project scoped remote tool catalog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L348) | -| `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L984) | -| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1015) | +| `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L983) | +| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1033) | | `createSleepTool` | Create sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L54) | | `createToolsFromHostDefinitions` | Create tools from host definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/host-tools.ts#L96) | | `createToolsFromRemoteDefinitions` | Create tools from remote definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L29) | @@ -170,7 +170,7 @@ Create a typed tool definition. | `ProjectScopedRemoteToolExecutionInput` | Input payload for project scoped remote tool execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L38) | | `ProjectScopedRemoteToolOptions` | Options accepted by project scoped remote tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L6) | | `RemoteMCPToolSourceConfig` | Configuration used by remote MCP tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L52) | -| `RemoteMCPToolSourceTransportOptions` | Deployment-owned transport policy for trusted MCP endpoint roots. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L991) | +| `RemoteMCPToolSourceTransportOptions` | Deployment-owned transport policy for trusted MCP endpoint roots. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L990) | | `RemoteToolMaterializationOptions` | Options accepted by remote tool materialization. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L8) | | `RemoteToolSource` | Remote tool source loaded dynamically at runtime. Hosts can provide these to expose tools from remote MCP-compatible systems without registering those tools globally inside the framework. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/types.ts#L231) | | `SleepToolInput` | Input payload for sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L45) | diff --git a/src/agent/hosted/project-remote-tool-source.test.ts b/src/agent/hosted/project-remote-tool-source.test.ts index 21e96116f3..66aac4706b 100644 --- a/src/agent/hosted/project-remote-tool-source.test.ts +++ b/src/agent/hosted/project-remote-tool-source.test.ts @@ -905,7 +905,7 @@ Deno.test("createHostedProjectRemoteToolSources infers Studio MCP from allowed S ); }); -Deno.test("createHostedProjectRemoteToolSources does not infer Studio when explicit API-only MCP is set", async () => { +it("does not infer Studio when explicit API-only MCP is set", async () => { const configs: RemoteMCPToolSourceConfig[] = []; const sources = createHostedProjectRemoteToolSources({ authToken: "token-1", diff --git a/src/agent/service/mcp-server-config.test.ts b/src/agent/service/mcp-server-config.test.ts index 8a811935e5..11c82a926d 100644 --- a/src/agent/service/mcp-server-config.test.ts +++ b/src/agent/service/mcp-server-config.test.ts @@ -1,4 +1,5 @@ import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; import { createAgentServiceRemoteMcpConfig, createProjectScopedMcpUrl, @@ -56,26 +57,28 @@ Deno.test("createAgentServiceRemoteMcpConfig builds Veryfront API MCP config", a ); }); -Deno.test("createProjectScopedMcpUrl normalizes and replaces the project segment", () => { - assertEquals( - createProjectScopedMcpUrl("https://api.example", " project/1 "), - "https://api.example/projects/project%2F1/mcp", - ); - assertEquals( - createProjectScopedMcpUrl("https://api.example/projects/old/mcp", "new"), - "https://api.example/projects/new/mcp", - ); - assertEquals( - createProjectScopedMcpUrl("https://api.example/mcp", " "), - "https://api.example/mcp", - ); - assertEquals( - createProjectScopedMcpUrl( - "https://api.example/mcp/?environment=staging", - "project-1", - ), - "https://api.example/projects/project-1/mcp?environment=staging", - ); +describe("createProjectScopedMcpUrl", () => { + it("normalizes and replaces the project segment", () => { + assertEquals( + createProjectScopedMcpUrl("https://api.example", " project/1 "), + "https://api.example/projects/project%2F1/mcp", + ); + assertEquals( + createProjectScopedMcpUrl("https://api.example/projects/old/mcp", "new"), + "https://api.example/projects/new/mcp", + ); + assertEquals( + createProjectScopedMcpUrl("https://api.example/mcp", " "), + "https://api.example/mcp", + ); + assertEquals( + createProjectScopedMcpUrl( + "https://api.example/mcp/?environment=staging", + "project-1", + ), + "https://api.example/projects/project-1/mcp?environment=staging", + ); + }); }); Deno.test("createAgentServiceRemoteMcpConfig builds generic MCP config without dropping options", () => { diff --git a/src/tool/remote-mcp.test.ts b/src/tool/remote-mcp.test.ts index 4b1a05754b..4b035e85a9 100644 --- a/src/tool/remote-mcp.test.ts +++ b/src/tool/remote-mcp.test.ts @@ -113,12 +113,14 @@ describe("tool/remote-mcp", () => { assertEquals(transportCalls, 0); }); - it("uses host transport for a dynamic project-scoped trusted endpoint", async () => { + it("uses host transport for dynamic project-scoped endpoints with query parameters", async () => { let transportCalls = 0; + const requestUrls: string[] = []; const createSource = createRemoteMCPToolSourceFactoryWithTransport({ trustedEndpoints: ["http://veryfront-api/mcp"], - requestFetch: async (_input, init) => { + requestFetch: async (input, init) => { transportCalls++; + requestUrls.push(String(input)); const body = JSON.parse(String(init && "body" in init ? init.body : undefined)) as { id: string; }; @@ -127,13 +129,17 @@ describe("tool/remote-mcp", () => { }); let projectId = "project-1"; const source = createSource({ - endpoint: () => `http://veryfront-api/projects/${projectId}/mcp`, + endpoint: () => `http://veryfront-api/projects/${projectId}/mcp?environment=staging`, }); assertEquals(await source.listTools(), []); projectId = "project-2"; assertEquals(await source.listTools(), []); assertEquals(transportCalls, 2); + assertEquals(requestUrls, [ + "http://veryfront-api/projects/project-1/mcp?environment=staging", + "http://veryfront-api/projects/project-2/mcp?environment=staging", + ]); }); it("rejects invalid trusted endpoints when the factory is created", () => { @@ -362,7 +368,7 @@ describe("tool/remote-mcp", () => { await withEnv({ VERYFRONT_API_BASE_URL: "https://93.184.216.34" }, async () => { const source = createRemoteMCPToolSource({ id: "veryfront-mcp", - endpoint: "https://93.184.216.34/projects/project-1/mcp", + endpoint: "https://93.184.216.34/projects/project-1/mcp?environment=staging", }); await withMockFetch( diff --git a/src/tool/remote-mcp.ts b/src/tool/remote-mcp.ts index 58e2d57b5a..4a2387397a 100644 --- a/src/tool/remote-mcp.ts +++ b/src/tool/remote-mcp.ts @@ -813,11 +813,10 @@ function normalizeCallToolResult(input: { function endpointBindsToolAuthorization(endpoint: string): boolean { const apiBaseUrl = getApiBaseUrlEnv(); if (typeof apiBaseUrl !== "string" || apiBaseUrl.length === 0) return false; - const normalizedEndpoint = normalizeTrustedEndpoint(endpoint); - if (!normalizedEndpoint) return false; + const endpointUrl = parseMcpRequestEndpoint(endpoint); + if (!endpointUrl) return false; try { - const endpointUrl = new URL(normalizedEndpoint); const apiBase = new URL(apiBaseUrl); if (endpointUrl.origin !== apiBase.origin) return false; @@ -1006,6 +1005,25 @@ function normalizeTrustedEndpoint(value: string): string | undefined { } } +/** Parse a safe MCP request URL while preserving its query string. */ +function parseMcpRequestEndpoint(value: string): URL | undefined { + try { + const url = new URL(value); + if (url.protocol !== "http:" && url.protocol !== "https:") return undefined; + if (url.username || url.password || url.hash) return undefined; + return url; + } catch { + return undefined; + } +} + +function normalizeMcpRequestEndpoint(value: string): string | undefined { + const url = parseMcpRequestEndpoint(value); + if (!url) return undefined; + url.search = ""; + return url.toString(); +} + /** * Create a remote MCP source factory with narrowly scoped host transport. * @@ -1038,7 +1056,7 @@ function isTrustedDeploymentMcpEndpoint( endpoint: string, trustedEndpoints: ReadonlySet, ): boolean { - const normalizedEndpoint = normalizeTrustedEndpoint(endpoint); + const normalizedEndpoint = normalizeMcpRequestEndpoint(endpoint); if (!normalizedEndpoint) return false; if (trustedEndpoints.has(normalizedEndpoint)) return true; From 817a7942ecd3516ec491b948b0c29ebfcd96627a Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 08:53:18 +0200 Subject: [PATCH 6/9] test(agent): use portable MCP config tests --- src/agent/service/mcp-server-config.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent/service/mcp-server-config.test.ts b/src/agent/service/mcp-server-config.test.ts index 11c82a926d..497f0de303 100644 --- a/src/agent/service/mcp-server-config.test.ts +++ b/src/agent/service/mcp-server-config.test.ts @@ -6,14 +6,14 @@ import { defaultAgentServiceMcpServers, } from "./mcp-server-config.ts"; -Deno.test("defaultAgentServiceMcpServers enables first-party MCP servers", () => { +it("defaultAgentServiceMcpServers enables first-party MCP servers", () => { assertEquals(defaultAgentServiceMcpServers(), [ { kind: "veryfront-api" }, { kind: "veryfront-studio" }, ]); }); -Deno.test("createAgentServiceRemoteMcpConfig builds Veryfront API MCP config", async () => { +it("createAgentServiceRemoteMcpConfig builds Veryfront API MCP config", async () => { let projectId = "project-1"; const config = createAgentServiceRemoteMcpConfig({ server: { kind: "veryfront-api" }, @@ -81,7 +81,7 @@ describe("createProjectScopedMcpUrl", () => { }); }); -Deno.test("createAgentServiceRemoteMcpConfig builds generic MCP config without dropping options", () => { +it("createAgentServiceRemoteMcpConfig builds generic MCP config without dropping options", () => { const headers = { Authorization: "Bearer external-token" }; assertEquals( createAgentServiceRemoteMcpConfig({ @@ -105,7 +105,7 @@ Deno.test("createAgentServiceRemoteMcpConfig builds generic MCP config without d ); }); -Deno.test("createAgentServiceRemoteMcpConfig gates Studio MCP by client profile", async () => { +it("createAgentServiceRemoteMcpConfig gates Studio MCP by client profile", async () => { const blockedConfig = createAgentServiceRemoteMcpConfig({ server: { kind: "veryfront-studio" }, authToken: "token-1", From dbd9a2cfa28f0c7215d6618f80dd4e0ad8d7a349 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 09:08:08 +0200 Subject: [PATCH 7/9] fix(agent): harden scoped MCP transport --- docs/api-reference/veryfront/tool.md | 2 +- src/agent/service/mcp-server-config.test.ts | 4 +++ src/agent/service/mcp-server-config.ts | 8 ++++- src/tool/remote-mcp.test.ts | 33 +++++++++++++++++++++ src/tool/remote-mcp.ts | 25 ++++++++++------ 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/docs/api-reference/veryfront/tool.md b/docs/api-reference/veryfront/tool.md index e416c5b83f..1b8055667a 100644 --- a/docs/api-reference/veryfront/tool.md +++ b/docs/api-reference/veryfront/tool.md @@ -128,7 +128,7 @@ Create a typed tool definition. | `createContext7ToolSource` | Create context7 tool source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/context7.ts#L28) | | `createProjectScopedRemoteToolCatalog` | Create project scoped remote tool catalog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/project-scoped-remote-tools.ts#L348) | | `createRemoteMCPToolSource` | Create a remote MCP source with the framework's guarded outbound transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L983) | -| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1033) | +| `createRemoteMCPToolSourceFactoryWithTransport` | Create a remote MCP source factory with narrowly scoped host transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-mcp.ts#L1038) | | `createSleepTool` | Create sleep tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/sleep.ts#L54) | | `createToolsFromHostDefinitions` | Create tools from host definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/host-tools.ts#L96) | | `createToolsFromRemoteDefinitions` | Create tools from remote definitions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/tool/remote-source-tools.ts#L29) | diff --git a/src/agent/service/mcp-server-config.test.ts b/src/agent/service/mcp-server-config.test.ts index 497f0de303..0d2e1e786b 100644 --- a/src/agent/service/mcp-server-config.test.ts +++ b/src/agent/service/mcp-server-config.test.ts @@ -78,6 +78,10 @@ describe("createProjectScopedMcpUrl", () => { ), "https://api.example/projects/project-1/mcp?environment=staging", ); + assertEquals( + createProjectScopedMcpUrl("not an absolute URL", "project-1"), + "not an absolute URL", + ); }); }); diff --git a/src/agent/service/mcp-server-config.ts b/src/agent/service/mcp-server-config.ts index 158571800b..90d9f6b7cb 100644 --- a/src/agent/service/mcp-server-config.ts +++ b/src/agent/service/mcp-server-config.ts @@ -53,7 +53,13 @@ export function createProjectScopedMcpUrl( const normalizedProjectId = projectId?.trim(); if (!normalizedProjectId) return apiMcpUrl; - const url = new URL(apiMcpUrl); + let url: URL; + try { + url = new URL(apiMcpUrl); + } catch { + // Let the remote MCP boundary produce its standard configuration error. + return apiMcpUrl; + } const basePath = url.pathname .replace(/\/projects\/[^/]+\/mcp\/?$/, "") .replace(/\/mcp\/?$/, "") diff --git a/src/tool/remote-mcp.test.ts b/src/tool/remote-mcp.test.ts index 4b035e85a9..b335018b0f 100644 --- a/src/tool/remote-mcp.test.ts +++ b/src/tool/remote-mcp.test.ts @@ -142,6 +142,39 @@ describe("tool/remote-mcp", () => { ]); }); + it("uses the request URL's origin and path, not query values, for trusted transport", async () => { + let transportCalls = 0; + const createSource = createRemoteMCPToolSourceFactoryWithTransport({ + trustedEndpoints: ["http://veryfront-api/mcp"], + requestFetch: async (_input, init) => { + transportCalls++; + const body = JSON.parse(String(init && "body" in init ? init.body : undefined)) as { + id: string; + }; + return Response.json({ jsonrpc: "2.0", id: body.id, result: { tools: [] } }); + }, + }); + + await createSource({ + endpoint: "http://veryfront-api/mcp?redirect=http://169.254.169.254/latest/meta-data", + }).listTools(); + assertEquals(transportCalls, 1); + + for ( + const endpoint of [ + "http://veryfront-api.evil.com/mcp?target=http://veryfront-api/mcp", + "http://veryfront-api/mcp/../../../admin?target=/mcp", + ] + ) { + await assertRejects( + () => createSource({ endpoint }).listTools(), + Error, + "Outbound network egress blocked", + ); + } + assertEquals(transportCalls, 1); + }); + it("rejects invalid trusted endpoints when the factory is created", () => { const error = assertThrows( () => diff --git a/src/tool/remote-mcp.ts b/src/tool/remote-mcp.ts index 4a2387397a..8cfab202a2 100644 --- a/src/tool/remote-mcp.ts +++ b/src/tool/remote-mcp.ts @@ -994,18 +994,23 @@ export interface RemoteMCPToolSourceTransportOptions { requestFetch: typeof fetch; } -function normalizeTrustedEndpoint(value: string): string | undefined { +function parseTrustedEndpoint(value: string): URL | undefined { try { const url = new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") return undefined; if (url.username || url.password || url.search || url.hash) return undefined; - return url.toString(); + return url; } catch { return undefined; } } -/** Parse a safe MCP request URL while preserving its query string. */ +/** + * Parse a safe MCP request URL while preserving its query string. + * + * Trusted transport is selected from the parsed scheme, origin, and path only; + * query parameters remain part of the request URL and cannot change that target. + */ function parseMcpRequestEndpoint(value: string): URL | undefined { try { const url = new URL(value); @@ -1034,19 +1039,21 @@ export function createRemoteMCPToolSourceFactoryWithTransport( options: RemoteMCPToolSourceTransportOptions, ): (config: RemoteMCPToolSourceConfig) => RemoteToolSource { const trustedEndpoints = new Set(); + const trustedEndpointUrls: URL[] = []; for (const value of options.trustedEndpoints) { - const endpoint = normalizeTrustedEndpoint(value); - if (!endpoint) { + const endpointUrl = parseTrustedEndpoint(value); + if (!endpointUrl) { throw new TypeError("Invalid trusted endpoint"); } - trustedEndpoints.add(endpoint); + trustedEndpoints.add(endpointUrl.toString()); + trustedEndpointUrls.push(endpointUrl); } return (config) => createRemoteMCPToolSourceWithFetch( config, (endpoint) => - isTrustedDeploymentMcpEndpoint(endpoint, trustedEndpoints) + isTrustedDeploymentMcpEndpoint(endpoint, trustedEndpoints, trustedEndpointUrls) ? options.requestFetch : guardedOutboundFetch, ); @@ -1055,6 +1062,7 @@ export function createRemoteMCPToolSourceFactoryWithTransport( function isTrustedDeploymentMcpEndpoint( endpoint: string, trustedEndpoints: ReadonlySet, + trustedEndpointUrls: readonly URL[], ): boolean { const normalizedEndpoint = normalizeMcpRequestEndpoint(endpoint); if (!normalizedEndpoint) return false; @@ -1067,8 +1075,7 @@ function isTrustedDeploymentMcpEndpoint( return false; } - for (const trustedEndpoint of trustedEndpoints) { - const trustedUrl = new URL(trustedEndpoint); + for (const trustedUrl of trustedEndpointUrls) { if (endpointUrl.origin !== trustedUrl.origin) continue; const trustedPath = trustedUrl.pathname.replace(/\/+$/, ""); From 3c2a5af570e54899eb0ea4d8def68d03b30c4fca Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Wed, 12 Aug 2026 09:37:56 +0200 Subject: [PATCH 8/9] test(agent): cover runtime project switch MCP scope --- .../veryfront-cloud-agent-service.test.ts | 95 +++++++++++++++++-- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/src/agent/hosted/veryfront-cloud-agent-service.test.ts b/src/agent/hosted/veryfront-cloud-agent-service.test.ts index bbc61025c4..2d58a53a19 100644 --- a/src/agent/hosted/veryfront-cloud-agent-service.test.ts +++ b/src/agent/hosted/veryfront-cloud-agent-service.test.ts @@ -1,4 +1,5 @@ import { toolRegistryInternal } from "#veryfront/tool/registry.ts"; +import { clearModelProviders, registerModelProvider } from "#veryfront/provider"; import "#veryfront/schemas/_test-setup.ts"; import { assert, @@ -24,6 +25,7 @@ import { toolRegistry, } from "#veryfront/tool"; import { defineSchema } from "#veryfront/schemas/index.ts"; +import { withMockFetch } from "#veryfront/testing/mock-fetch.ts"; import { __resetLogRecordEmitterForTests, agentLogger } from "#veryfront/utils/logger/index.ts"; import { createExecuteSkillScriptTool, @@ -73,6 +75,8 @@ Deno.test("public agent service options expose deployment-owned remote MCP compo Deno.test("root and child runtimes use the deployment-owned remote MCP factory", async () => { const createdConfigs: RemoteMCPToolSourceConfig[] = []; let failStudioListing = false; + let modelCallCount = 0; + let switchedTaskContext: { projectId: string; projectSlug?: string } | undefined; const injectedFactory = (config: RemoteMCPToolSourceConfig): RemoteToolSource => { createdConfigs.push(config); return { @@ -80,8 +84,21 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", listTools: () => failStudioListing && config.endpoint === "https://studio.example/mcp" ? Promise.reject(new Error("stop after transport capture")) - : Promise.resolve([]), - executeTool: () => Promise.resolve(null), + : Promise.resolve( + config.id === "studio-mcp" + ? [{ + name: "studio_open_project", + description: "Open a project.", + parameters: { type: "object", properties: {} }, + }] + : [], + ), + executeTool: (toolName) => + Promise.resolve( + config.id === "studio-mcp" && toolName === "studio_open_project" + ? { success: true, project_id: "project-2", slug: "project-two" } + : null, + ), }; }; const context = { @@ -92,8 +109,8 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", }, infrastructure: { getConfig: () => ({ - VERYFRONT_API_URL: "https://api.example", - VERYFRONT_MCP_URL: "https://api.example/mcp", + VERYFRONT_API_URL: "https://93.184.216.34", + VERYFRONT_MCP_URL: "https://93.184.216.34/mcp", VERYFRONT_STUDIO_MCP_URL: "https://studio.example/mcp", VERYFRONT_ENABLE_DURABLE_INVOKE_AGENT: false, }), @@ -123,6 +140,10 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", inputSchema: defineSchema((v) => v.object({}))(), execute: () => ({ ok: true }), }), + refreshProjectSkillIds: (taskContext: { projectId: string; projectSlug?: string }) => { + switchedTaskContext = taskContext; + return Promise.resolve(); + }, }]]), trace: (_name: string, operation: () => unknown) => operation(), } as never; @@ -133,13 +154,43 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", capabilities: ["ui_panels"], }; - await createAgentRuntime(context, { + clearModelProviders(); + registerModelProvider("test", () => ({ + provider: "test", + modelId: "test/hosted-project-switch", + doGenerate: () => Promise.reject(new Error("unused")), + doStream: () => { + modelCallCount++; + return Promise.resolve({ + stream: new ReadableStream({ + start(controller) { + if (modelCallCount === 1) { + controller.enqueue({ + type: "tool-call", + toolCallId: "open-project-1", + toolName: "studio_open_project", + input: { project_reference: "project-two" }, + }); + controller.enqueue({ type: "finish", finishReason: "tool-calls", usage: {} }); + } else { + controller.enqueue({ type: "text-delta", text: "opened" }); + controller.enqueue({ type: "finish", finishReason: "stop", usage: {} }); + } + controller.close(); + }, + }), + }); + }, + })); + + const rootRuntime = await createAgentRuntime(context, { projectId: "project-1", branchId: "branch-1", authToken: "token-1", instructions: "Use the available tools.", agentId: "root-agent", - allowedTools: [], + model: "test/hosted-project-switch", + allowedTools: ["studio_open_project"], allowDelegation: false, clientProfile, }); @@ -149,16 +200,42 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", endpoint: typeof endpoint === "function" ? await endpoint() : endpoint, }))), [ - { id: "veryfront-mcp", endpoint: "https://api.example/projects/project-1/mcp" }, + { id: "veryfront-mcp", endpoint: "https://93.184.216.34/projects/project-1/mcp" }, { id: "studio-mcp", endpoint: "https://studio.example/mcp" }, ], ); + try { + await withMockFetch( + () => Promise.resolve(Response.json({ tools: [] })), + async () => { + const stream = await rootRuntime.agent.stream({ + messages: [], + abortSignal: new AbortController().signal, + }); + for await (const _chunk of stream.toUIMessageStream()) { + // Consume the project-switch tool round trip. + } + }, + ); + } finally { + await rootRuntime.cleanup(); + clearModelProviders(); + } + assertEquals(switchedTaskContext?.projectId, "project-2"); + const rootApiConfig = createdConfigs.find((config) => config.id === "veryfront-mcp"); + assertEquals( + typeof rootApiConfig?.endpoint === "function" + ? await rootApiConfig.endpoint() + : rootApiConfig?.endpoint, + "https://93.184.216.34/projects/project-2/mcp", + ); + createdConfigs.length = 0; failStudioListing = true; const invokeAgent = createInvokeAgentTool(context, { authToken: "token-1", - projectId: "project-1", + projectId: switchedTaskContext?.projectId ?? "project-1", branchId: "branch-1", agentId: "orchestrator", clientProfile, @@ -174,7 +251,7 @@ Deno.test("root and child runtimes use the deployment-owned remote MCP factory", endpoint: typeof endpoint === "function" ? await endpoint() : endpoint, }))), [ - { id: "veryfront-mcp-fork", endpoint: "https://api.example/projects/project-1/mcp" }, + { id: "veryfront-mcp-fork", endpoint: "https://93.184.216.34/projects/project-2/mcp" }, { id: "studio-mcp-live-tools", endpoint: "https://studio.example/mcp" }, ], ); From bcbc03a196944fe25f8b495deac40331205e5ee6 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Wed, 12 Aug 2026 10:35:48 +0200 Subject: [PATCH 9/9] docs: regenerate API reference after merge with main child-fork-tool-sources.ts gained a line (execution-support.ts changes from #3632), shifting the source link for prepareDefaultHostedChildForkSandboxToolSources from L196 to L197. --- docs/api-reference/veryfront/agent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index a0ef5be75d..685ad68ecd 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -922,7 +922,7 @@ Input delivered to a hosted agent-service detached execution callback. | `prepareConversationRunExternalEvents` | Prepare conversation run external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L78) | | `prepareConversationRunStreamEvents` | Prepare conversation run stream events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-preparation.ts#L59) | | `prepareDefaultHostedChildForkRuntimeTools` | Prepare default hosted child fork runtime tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L312) | -| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L196) | +| `prepareDefaultHostedChildForkSandboxToolSources` | Prepare default hosted child fork sandbox tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L197) | | `prepareDefaultHostedChildForkToolAssembly` | Prepare default hosted child fork tool assembly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-requested-tools.ts#L366) | | `prepareDefaultHostedChildForkToolSources` | Prepare default hosted child fork tool sources. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-tool-sources.ts#L88) | | `prepareHostedChatExecution` | Prepare hosted chat execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L482) |