mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-09-16 14:51:37 +02:00
Fix MCP send_http_request ignoring environmentId (#654)
This commit is contained in:
@@ -33,3 +33,26 @@ Restart Claude Desktop and make sure Yaak is running.
|
||||
- `get_environment_id` - Get the current environment ID
|
||||
- `copy_to_clipboard` - Copy text to the system clipboard
|
||||
- `show_toast` - Show a toast notification in Yaak
|
||||
|
||||
## Environment Selection
|
||||
|
||||
`send_http_request` accepts an optional `environmentId` for that execution. It overrides the
|
||||
active environment without changing the selection in Yaak. Omitting it uses the host's
|
||||
active environment, as before.
|
||||
|
||||
The ID must identify a base or sub-environment in the request's workspace. An empty,
|
||||
unknown, foreign-workspace, or folder environment ID returns an error before sending.
|
||||
Use the base environment ID to run with global variables only. Folder variables still
|
||||
apply according to the request's folder hierarchy.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "rq_example",
|
||||
"workspaceId": "wk_example",
|
||||
"environmentId": "ev_staging"
|
||||
}
|
||||
```
|
||||
|
||||
This requires a Yaak host with support for the `environmentId` plugin API field;
|
||||
older hosts ignore it. `get_environment_id` reports the active selection, not the
|
||||
override used by a previous send.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { Context } from "@yaakapp/api";
|
||||
import { afterEach, expect, test, vi } from "vite-plus/test";
|
||||
import { registerHttpRequestTools } from "./httpRequest";
|
||||
|
||||
const cleanups: Array<() => Promise<void>> = [];
|
||||
afterEach(async () => {
|
||||
for (const cleanup of cleanups.splice(0)) await cleanup();
|
||||
});
|
||||
|
||||
async function fixture({ missing = false, sendError = false } = {}) {
|
||||
const httpRequest = { id: "rq_test", workspaceId: "wk_test", url: "http://localhost/echo" };
|
||||
const send = vi.fn(async (_args: { httpRequest: unknown; environmentId?: string }) => {
|
||||
if (sendError) throw new Error("Environment not found");
|
||||
return { httpResponse: { id: "rs_test", status: 200 } };
|
||||
});
|
||||
// Only the host is mocked; calls go through the real MCP client, schema and transport.
|
||||
const yaak = {
|
||||
workspace: {
|
||||
list: async () => [{ id: "wk_test", name: "Test Workspace" }],
|
||||
withContext: () => yaak,
|
||||
},
|
||||
httpRequest: { getById: async () => (missing ? null : httpRequest), send },
|
||||
};
|
||||
const server = new McpServer({ name: "yaak-test", version: "0.0.0" });
|
||||
registerHttpRequestTools(server, { yaak: yaak as unknown as Context });
|
||||
const client = new Client({ name: "test", version: "0.0.0" });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
cleanups.push(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
});
|
||||
return { client, send, httpRequest };
|
||||
}
|
||||
|
||||
test("forwards the advertised environmentId to the host", async () => {
|
||||
const { client, send, httpRequest } = await fixture();
|
||||
const { tools } = await client.listTools();
|
||||
expect(
|
||||
tools.find((tool) => tool.name === "send_http_request")?.inputSchema.properties,
|
||||
).toHaveProperty("environmentId");
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: httpRequest.id, workspaceId: "wk_test", environmentId: "ev_staging" },
|
||||
});
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(send).toHaveBeenCalledExactlyOnceWith({ httpRequest, environmentId: "ev_staging" });
|
||||
});
|
||||
|
||||
test("leaves the environment unspecified when omitted", async () => {
|
||||
const { client, send, httpRequest } = await fixture();
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: httpRequest.id },
|
||||
});
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(send).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ httpRequest }));
|
||||
expect(send.mock.calls[0]?.[0]).not.toHaveProperty("environmentId", expect.any(String));
|
||||
});
|
||||
|
||||
test("does not send a missing request", async () => {
|
||||
const { client, send } = await fixture({ missing: true });
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: "rq_missing" },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("returns host environment errors to the MCP client", async () => {
|
||||
const { client } = await fixture({ sendError: true });
|
||||
const result = await client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: "rq_test", environmentId: "ev_missing" },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toEqual([{ type: "text", text: "Environment not found" }]);
|
||||
});
|
||||
|
||||
test("keeps concurrent environment overrides separate", async () => {
|
||||
const { client, send, httpRequest } = await fixture();
|
||||
await Promise.all(
|
||||
["ev_a", "ev_b"].map((environmentId) =>
|
||||
client.callTool({
|
||||
name: "send_http_request",
|
||||
arguments: { id: httpRequest.id, environmentId },
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(send).toHaveBeenCalledTimes(2);
|
||||
expect(send).toHaveBeenCalledWith({ httpRequest, environmentId: "ev_a" });
|
||||
expect(send).toHaveBeenCalledWith({ httpRequest, environmentId: "ev_b" });
|
||||
});
|
||||
@@ -69,18 +69,24 @@ export function registerHttpRequestTools(server: McpServer, ctx: McpServerContex
|
||||
description: "Send an HTTP request and get the response",
|
||||
inputSchema: {
|
||||
id: z.string().describe("The HTTP request ID to send"),
|
||||
environmentId: z.string().optional().describe("Optional environment ID to use"),
|
||||
environmentId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Environment ID for this send only; defaults to the active environment"),
|
||||
workspaceId: workspaceIdSchema,
|
||||
},
|
||||
},
|
||||
async ({ id, workspaceId }) => {
|
||||
async ({ id, workspaceId, environmentId }) => {
|
||||
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
|
||||
const httpRequest = await workspaceCtx.yaak.httpRequest.getById({ id });
|
||||
if (httpRequest == null) {
|
||||
throw new Error(`HTTP request with ID ${id} not found`);
|
||||
}
|
||||
|
||||
const { httpResponse: response } = await workspaceCtx.yaak.httpRequest.send({ httpRequest });
|
||||
const { httpResponse: response } = await workspaceCtx.yaak.httpRequest.send({
|
||||
httpRequest,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
||||
Reference in New Issue
Block a user