Fix MCP send_http_request ignoring environmentId (#654)

This commit is contained in:
Ricardo Lopes Temoteo
2026-09-15 14:04:38 -07:00
committed by GitHub
parent 4859aba142
commit ab616fd915
11 changed files with 619 additions and 8 deletions
+203 -1
View File
@@ -182,6 +182,31 @@ async fn build_plugin_reply(
http_request.workspace_id = workspace_id; http_request.workspace_id = workspace_id;
} }
let environment_id = if let Some(environment_id) =
send_http_request_request.environment_id.as_deref()
{
if shared_workspace_id.is_some_and(|id| id != http_request.workspace_id) {
return Some(InternalEventPayload::ErrorResponse(ErrorResponse {
error: "HTTP request does not belong to the selected workspace"
.to_string(),
}));
}
match host_context
.query_manager
.connect()
.get_environment_for_workspace(&http_request.workspace_id, environment_id)
{
Ok(environment) => Some(environment.id),
Err(err) => {
return Some(InternalEventPayload::ErrorResponse(ErrorResponse {
error: err.to_string(),
}));
}
}
} else {
execution_context.environment_id.clone()
};
let cookie_jar_id = let cookie_jar_id =
if let Some(cookie_jar_id) = execution_context.cookie_jar_id.clone() { if let Some(cookie_jar_id) = execution_context.cookie_jar_id.clone() {
Some(cookie_jar_id) Some(cookie_jar_id)
@@ -211,7 +236,7 @@ async fn build_plugin_reply(
query_manager: &host_context.query_manager, query_manager: &host_context.query_manager,
blob_manager: &host_context.blob_manager, blob_manager: &host_context.blob_manager,
request: http_request, request: http_request,
environment_id: execution_context.environment_id.as_deref(), environment_id: environment_id.as_deref(),
update_source: UpdateSource::Plugin, update_source: UpdateSource::Plugin,
cookie_jar_id, cookie_jar_id,
response_dir: &host_context.response_dir, response_dir: &host_context.response_dir,
@@ -1003,3 +1028,180 @@ fn prompt_label_for_base(base: &yaak_plugins::events::FormInputBase) -> String {
} }
base.name.clone() base.name.clone()
} }
#[cfg(test)]
mod environment_tests {
use super::*;
use serde_json::json;
use tempfile::TempDir;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::time::{Duration, timeout};
use yaak_models::models::{EnvironmentVariable, HttpRequest, HttpRequestHeader};
#[tokio::test]
async fn plugin_send_uses_override_and_fallback_and_rejects_invalid_ids_before_sending() {
let dir = TempDir::new().unwrap();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let (received_tx, mut received_rx) = tokio::sync::mpsc::unbounded_channel();
let server = tokio::spawn(async move {
loop {
let (mut socket, _) = listener.accept().await.unwrap();
let mut request = Vec::new();
loop {
let mut chunk = [0u8; 1024];
let n = socket.read(&mut chunk).await.unwrap();
if n == 0 {
break;
}
request.extend_from_slice(&chunk[..n]);
if request.windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
}
let request = String::from_utf8(request).unwrap();
eprintln!("Local HTTP test received on {address}:\n{request}");
received_tx.send(request).unwrap();
socket
.write_all(
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok",
)
.await
.unwrap();
}
});
let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(
&dir.path().join("db.sqlite"),
&dir.path().join("blobs.sqlite"),
)
.unwrap();
let (base, a, b, request) = query_manager
.with_tx(|db| {
let source = &UpdateSource::Background;
let workspace = db.list_workspaces()?.remove(0);
let vars = |value: &str| {
vec![EnvironmentVariable {
enabled: true,
name: "marker".into(),
value: value.into(),
..Default::default()
}]
};
let base = db.ensure_base_environment(&workspace.id)?;
let base = db.upsert_environment(
&Environment { variables: vars("global"), ..base },
source,
)?;
let sub = |name: &str| {
db.upsert_environment(
&Environment {
name: name.into(),
workspace_id: workspace.id.clone(),
parent_model: "environment".into(),
parent_id: Some(base.id.clone()),
variables: vars(name),
..Default::default()
},
source,
)
};
let a = sub("a")?;
let b = sub("b")?;
let request = db.upsert_http_request(
&HttpRequest {
workspace_id: workspace.id,
url: format!("http://{address}/echo"),
method: "GET".into(),
headers: vec![HttpRequestHeader {
enabled: true,
name: "X-Environment".into(),
value: "${[ marker ]}".into(),
..Default::default()
}],
..Default::default()
},
source,
)?;
Ok::<_, yaak_models::error::Error>((base, a, b, request))
})
.unwrap();
let plugin_dir = dir.path().join("plugins");
std::fs::create_dir_all(&plugin_dir).unwrap();
let plugin_manager = Arc::new(
PluginManager::new(
plugin_dir.clone(),
plugin_dir,
PathBuf::from("node"),
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs"),
&query_manager,
&PluginContext::new_empty(),
false,
)
.await
.unwrap(),
);
let host = CliHostContext {
encryption_manager: Arc::new(EncryptionManager::new(
query_manager.clone(),
"yaak-test",
)),
query_manager,
blob_manager,
plugin_manager: plugin_manager.clone(),
connection_manager: Arc::new(HttpConnectionManager::new()),
response_dir: dir.path().join("responses"),
execution_context: CliExecutionContext {
workspace_id: Some(base.workspace_id.clone()),
environment_id: Some(a.id.clone()),
..Default::default()
},
};
std::fs::create_dir_all(&host.response_dir).unwrap();
let event = |environment_id: Option<&str>| -> InternalEvent {
let mut payload =
json!({ "type": "send_http_request_request", "httpRequest": request });
if let Some(id) = environment_id {
payload["environmentId"] = json!(id);
}
serde_json::from_value(json!({
"id": "test", "pluginRefId": "test", "pluginName": "test", "replyId": null,
"context": PluginContext::new_empty(), "payload": payload
}))
.unwrap()
};
for (id, marker) in [
(Some(b.id.as_str()), "b"),
(None, "a"),
(Some(base.id.as_str()), "global"),
] {
let reply =
timeout(Duration::from_secs(10), build_plugin_reply(&host, &event(id), "test"))
.await
.unwrap();
assert!(
matches!(reply, Some(InternalEventPayload::SendHttpRequestResponse(ref r)) if r.http_response.status == 200),
"{reply:?}"
);
let received = received_rx.recv().await.unwrap().to_lowercase();
assert!(received.contains(&format!("x-environment: {marker}\r\n")), "{received}");
assert_eq!(host.execution_context.environment_id.as_deref(), Some(a.id.as_str()));
}
for id in ["", "ev_missing"] {
let reply = build_plugin_reply(&host, &event(Some(id)), "test").await;
assert!(matches!(reply, Some(InternalEventPayload::ErrorResponse(_))), "{reply:?}");
}
assert!(received_rx.try_recv().is_err());
assert_eq!(
host.query_manager
.connect()
.list_http_responses_for_request(&request.id, None)
.unwrap()
.len(),
3
);
plugin_manager.terminate().await;
server.abort();
}
}
@@ -284,12 +284,26 @@ async fn handle_host_plugin_request<R: Runtime>(
let workspace = let workspace =
workspace_from_window(&window).expect("Failed to get workspace_id from window URL"); workspace_from_window(&window).expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window); let cookie_jar = cookie_jar_from_window(&window);
let environment = environment_from_window(&window);
if http_request.workspace_id.is_empty() { if http_request.workspace_id.is_empty() {
http_request.workspace_id = workspace.id; http_request.workspace_id = workspace.id.clone();
} }
let environment =
if let Some(environment_id) = req.environment_id.as_deref() {
if http_request.workspace_id != workspace.id {
return Err(crate::error::Error::GenericError(
"HTTP request does not belong to the selected workspace".to_string(),
));
}
Some(window.db().get_environment_for_workspace(
&http_request.workspace_id,
environment_id,
)?)
} else {
environment_from_window(&window)
};
let http_response = if http_request.id.is_empty() { let http_response = if http_request.id.is_empty() {
HttpResponse::default() HttpResponse::default()
} else { } else {
+3
View File
@@ -33,6 +33,9 @@ pub enum Error {
#[error("No base environment for {0}")] #[error("No base environment for {0}")]
MissingBaseEnvironment(String), MissingBaseEnvironment(String),
#[error("Invalid environment selection: {0}")]
InvalidEnvironment(String),
#[error("Multiple base environments for {0}. Delete duplicates before continuing.")] #[error("Multiple base environments for {0}. Delete duplicates before continuing.")]
MultipleBaseEnvironments(String), MultipleBaseEnvironments(String),
@@ -11,6 +11,27 @@ impl<'a> ClientDb<'a> {
self.find_one(EnvironmentIden::Id, id) self.find_one(EnvironmentIden::Id, id)
} }
/// Resolve an explicit execution environment, rejecting foreign and folder environments.
/// Folder variables are inherited from the request's folder, never selected globally.
pub fn get_environment_for_workspace(
&self,
workspace_id: &str,
environment_id: &str,
) -> Result<Environment> {
let environment = self.get_environment(environment_id)?;
if environment.workspace_id != workspace_id {
return Err(crate::error::Error::InvalidEnvironment(format!(
"Environment {environment_id} does not belong to workspace {workspace_id}"
)));
}
if !matches!(environment.parent_model.as_str(), "workspace" | "environment") {
return Err(crate::error::Error::InvalidEnvironment(format!(
"Environment {environment_id} is not a workspace environment"
)));
}
Ok(environment)
}
pub fn get_environment_by_folder_id(&self, folder_id: &str) -> Result<Option<Environment>> { pub fn get_environment_by_folder_id(&self, folder_id: &str) -> Result<Option<Environment>> {
let mut environments: Vec<Environment> = let mut environments: Vec<Environment> =
self.find_many(EnvironmentIden::ParentId, folder_id, None)?; self.find_many(EnvironmentIden::ParentId, folder_id, None)?;
@@ -196,3 +217,142 @@ impl<'a> WriteDb<'a> {
) )
} }
} }
#[cfg(test)]
mod tests {
use crate::error::Error;
use crate::init_in_memory;
use crate::models::{Environment, EnvironmentVariable, Folder, Workspace};
use crate::query_manager::QueryManager;
use crate::render::make_vars_hashmap;
use crate::util::UpdateSource;
fn fixture() -> (QueryManager, Environment, Environment, Environment) {
let (manager, _blobs, _rx) = init_in_memory().unwrap();
let source = &UpdateSource::Background;
let (base, staging, folder) = manager
.with_tx(|db| {
let workspace = db.list_workspaces()?.remove(0);
let variable = |name: &str, value: &str| EnvironmentVariable {
enabled: true,
name: name.into(),
value: value.into(),
..Default::default()
};
let base = db.ensure_base_environment(&workspace.id)?;
let base = db.upsert_environment(
&Environment {
variables: vec![
variable("marker", "global"),
variable("global_only", "inherited"),
],
..base
},
source,
)?;
let staging = db.upsert_environment(
&Environment {
workspace_id: workspace.id.clone(),
parent_model: "environment".into(),
parent_id: Some(base.id.clone()),
name: "Staging".into(),
variables: vec![variable("marker", "staging")],
..Default::default()
},
source,
)?;
let folder = db.upsert_folder(
&Folder {
workspace_id: workspace.id.clone(),
name: "Folder".into(),
..Default::default()
},
source,
)?;
let folder = db.upsert_environment(
&Environment {
workspace_id: workspace.id,
parent_model: "folder".into(),
parent_id: Some(folder.id),
variables: vec![variable("marker", "folder")],
..Default::default()
},
source,
)?;
Ok::<_, Error>((base, staging, folder))
})
.unwrap();
(manager, base, staging, folder)
}
#[test]
fn explicit_environment_preserves_global_and_folder_inheritance() {
let (manager, base, staging, folder) = fixture();
let db = manager.connect();
let selected = db.get_environment_for_workspace(&base.workspace_id, &staging.id).unwrap();
let vars = make_vars_hashmap(
db.resolve_environments(&base.workspace_id, None, Some(&selected.id)).unwrap(),
);
assert_eq!(vars["marker"], "staging");
assert_eq!(vars["global_only"], "inherited");
let vars = make_vars_hashmap(
db.resolve_environments(
&base.workspace_id,
folder.parent_id.as_deref(),
Some(&selected.id),
)
.unwrap(),
);
assert_eq!(vars["marker"], "folder");
assert_eq!(vars["global_only"], "inherited");
}
#[test]
fn base_environment_can_be_selected_explicitly() {
let (manager, base, _staging, _folder) = fixture();
let db = manager.connect();
let selected = db.get_environment_for_workspace(&base.workspace_id, &base.id).unwrap();
let vars = make_vars_hashmap(
db.resolve_environments(&base.workspace_id, None, Some(&selected.id)).unwrap(),
);
assert_eq!(vars["marker"], "global");
}
#[test]
fn explicit_environment_rejects_empty_and_missing_ids() {
let (manager, base, _staging, _folder) = fixture();
for id in ["", "ev_missing"] {
assert!(matches!(
manager.connect().get_environment_for_workspace(&base.workspace_id, id),
Err(Error::ModelNotFound(_))
));
}
}
#[test]
fn explicit_environment_rejects_another_workspace() {
let (manager, _base, staging, _folder) = fixture();
let other = manager
.with_tx(|db| {
db.upsert_workspace(
&Workspace { name: "Other".into(), ..Default::default() },
&UpdateSource::Background,
)
})
.unwrap();
assert!(matches!(
manager.connect().get_environment_for_workspace(&other.id, &staging.id),
Err(Error::InvalidEnvironment(_))
));
}
#[test]
fn explicit_environment_rejects_folder_variables() {
let (manager, base, _staging, folder) = fixture();
assert!(matches!(
manager.connect().get_environment_for_workspace(&base.workspace_id, &folder.id),
Err(Error::InvalidEnvironment(_))
));
}
}
+6 -1
View File
@@ -572,7 +572,12 @@ export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview"; export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, }; export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>,
/**
* Override the environment for this send without changing the active selection.
* When omitted, use the host's active environment.
*/
environmentId?: string, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, export type SendHttpRequestResponse = { httpResponse: HttpResponse,
/** /**
+30
View File
@@ -296,6 +296,36 @@ pub struct ExportHttpRequestResponse {
pub struct SendHttpRequestRequest { pub struct SendHttpRequestRequest {
#[ts(type = "Partial<HttpRequest>")] #[ts(type = "Partial<HttpRequest>")]
pub http_request: HttpRequest, pub http_request: HttpRequest,
/// Override the environment for this send without changing the active selection.
/// When omitted, use the host's active environment.
#[ts(optional)]
pub environment_id: Option<String>,
}
#[cfg(test)]
mod send_http_request_tests {
use super::SendHttpRequestRequest;
use serde_json::json;
#[test]
fn environment_override_survives_the_plugin_wire_format() {
let request: SendHttpRequestRequest = serde_json::from_value(json!({
"httpRequest": { "id": "rq_test" }, "environmentId": "ev_staging"
}))
.unwrap();
assert_eq!(request.environment_id.as_deref(), Some("ev_staging"));
assert_eq!(serde_json::to_value(request).unwrap()["environmentId"], "ev_staging");
}
#[test]
fn older_plugins_can_omit_the_environment_override() {
let request: SendHttpRequestRequest = serde_json::from_value(json!({
"httpRequest": { "id": "rq_test" }
}))
.unwrap();
assert_eq!(request.environment_id, None);
assert_eq!(request.http_request.id, "rq_test");
}
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
+6 -1
View File
@@ -572,7 +572,12 @@ export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview"; export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, }; export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>,
/**
* Override the environment for this send without changing the active selection.
* When omitted, use the host's active environment.
*/
environmentId?: string, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, export type SendHttpRequestResponse = { httpResponse: HttpResponse,
/** /**
@@ -0,0 +1,65 @@
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import type { InternalEvent } from "@yaakapp/api";
import { expect, test } from "vite-plus/test";
import { EventChannel } from "../src/EventChannel";
import { PluginInstance } from "../src/PluginInstance";
test("the runtime forwards per-send environments without changing plugin context", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "yaak-send-environment-"));
await mkdir(path.join(dir, "build"));
await writeFile(
path.join(dir, "build/index.js"),
`exports.plugin = { async init(ctx) {
await Promise.all(["ev_a", "ev_b", undefined].map(environmentId =>
ctx.httpRequest.send({ httpRequest: { id: "rq_test" }, environmentId })
));
} };`,
);
const channel = new EventChannel();
const context = { workspaceId: "wk_test", label: "test-window" };
const bootRequest = { dir, watch: false };
const instance = new PluginInstance({ bootRequest, pluginRefId: "test", context }, channel);
const sends: InternalEvent[] = [];
const booted = new Promise<void>((resolve, reject) => {
channel.listen((event) => {
if (event.payload.type === "send_http_request_request") {
// Match the JSON transport used by the real plugin host.
sends.push(JSON.parse(JSON.stringify(event)) as InternalEvent);
instance.postMessage({
...event,
id: `reply-${event.id}`,
replyId: event.id,
payload: { type: "error_response", error: "test host received send" },
});
} else if (event.payload.type === "error_response") {
// The stub host rejects the sends after capturing them.
if (event.payload.error === "test host received send") resolve();
else reject(new Error(event.payload.error));
} else if (event.payload.type === "boot_response") {
reject(new Error("Expected the test host's send error"));
}
});
});
try {
instance.postMessage({
id: "boot",
replyId: null,
pluginRefId: "test",
pluginName: "test",
context,
payload: { type: "boot_request", ...bootRequest },
});
await booted;
expect(sends.map((event) => event.payload)).toEqual([
{ type: "send_http_request_request", httpRequest: { id: "rq_test" }, environmentId: "ev_a" },
{ type: "send_http_request_request", httpRequest: { id: "rq_test" }, environmentId: "ev_b" },
{ type: "send_http_request_request", httpRequest: { id: "rq_test" } },
]);
expect(sends.map((event) => event.context)).toEqual([context, context, context]);
} finally {
await instance.terminate();
await rm(dir, { recursive: true, force: true });
}
});
+23
View File
@@ -33,3 +33,26 @@ Restart Claude Desktop and make sure Yaak is running.
- `get_environment_id` - Get the current environment ID - `get_environment_id` - Get the current environment ID
- `copy_to_clipboard` - Copy text to the system clipboard - `copy_to_clipboard` - Copy text to the system clipboard
- `show_toast` - Show a toast notification in Yaak - `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", description: "Send an HTTP request and get the response",
inputSchema: { inputSchema: {
id: z.string().describe("The HTTP request ID to send"), 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, workspaceId: workspaceIdSchema,
}, },
}, },
async ({ id, workspaceId }) => { async ({ id, workspaceId, environmentId }) => {
const workspaceCtx = await getWorkspaceContext(ctx, workspaceId); const workspaceCtx = await getWorkspaceContext(ctx, workspaceId);
const httpRequest = await workspaceCtx.yaak.httpRequest.getById({ id }); const httpRequest = await workspaceCtx.yaak.httpRequest.getById({ id });
if (httpRequest == null) { if (httpRequest == null) {
throw new Error(`HTTP request with ID ${id} not found`); 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 { return {
content: [ content: [