diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 52c9acf0..27dfe37c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -19,7 +19,7 @@ jobs: with: build: false - deploy-gateway: + deploy-app: name: Gateway needs: ci permissions: @@ -28,8 +28,8 @@ jobs: uses: ./.github/workflows/__cloudrun-deploy.yml with: environment: production - service_name: director-production-gateway - working-directory: apps/gateway + service_name: director-production-app + working-directory: apps/app migrations: true seed_enabled: false diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index bc3dca66..4f136408 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -20,7 +20,7 @@ jobs: with: build: false - deploy-gateway: + deploy-app: name: Gateway needs: ci permissions: @@ -29,7 +29,7 @@ jobs: uses: ./.github/workflows/__cloudrun-deploy.yml with: environment: staging - service_name: director-staging-gateway + service_name: director-staging-app working-directory: apps/gateway migrations: true seed_enabled: true diff --git a/apps/cli/README.md b/apps/cli/README.md index f99173eb..99a02dcf 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -176,7 +176,7 @@ CORE COMMANDS add [options] Add a server to a playbook. remove Remove a server from a playbook update [serverName] [options] Update playbook attributes - http2stdio Proxy an HTTP connection (sse or streamable) to a stdio stream + http2stdio Proxy an HTTP connection to a stdio stream env [options] Print environment variables status Get the status of the director diff --git a/apps/cli/src/commands/core.ts b/apps/cli/src/commands/core.ts index 4d0a33ac..dc3a2317 100644 --- a/apps/cli/src/commands/core.ts +++ b/apps/cli/src/commands/core.ts @@ -112,9 +112,7 @@ export function registerCoreCommands(program: DirectorCommand): void { program .command("http2stdio ") - .description( - "Proxy an HTTP connection (sse or streamable) to a stdio stream", - ) + .description("Proxy an HTTP connection to a stdio stream") .action(async (url) => { await proxyHTTPToStdio(url); }); diff --git a/apps/cli/src/commands/core/add.ts b/apps/cli/src/commands/core/add.ts index 61ad31c5..47375673 100644 --- a/apps/cli/src/commands/core/add.ts +++ b/apps/cli/src/commands/core/add.ts @@ -25,7 +25,7 @@ export function registerAddCommand(program: DirectorCommand) { .addOption( makeOption({ flags: "-u,--url ", - description: "add a streamable or sse server by specifying the url", + description: "add an HTTP server by specifying the url", }), ) .addOption( diff --git a/apps/cli/src/commands/core/connect.ts b/apps/cli/src/commands/core/connect.ts index febfdf94..9e1a5bef 100644 --- a/apps/cli/src/commands/core/connect.ts +++ b/apps/cli/src/commands/core/connect.ts @@ -27,9 +27,8 @@ export function registerConnectCommand(program: DirectorCommand) { playbookId, }); - // Build full URLs with API key + // Build full URL with API key const streamableUrlWithKey = `${connectionInfo.streamableUrl}?key=${connectionInfo.apiKey}`; - const sseUrlWithKey = `${connectionInfo.sseUrl}?key=${connectionInfo.apiKey}`; // Build stdio command config const stdioCommand = { @@ -51,7 +50,6 @@ export function registerConnectCommand(program: DirectorCommand) { clientId: options.target as ClientId, name: connectionInfo.playbookId, connectionDetails: { - sseUrl: sseUrlWithKey, streamableUrl: streamableUrlWithKey, }, }); @@ -71,7 +69,6 @@ export function registerConnectCommand(program: DirectorCommand) { console.log( whiteBold("HTTP Streamable:") + " " + streamableUrlWithKey, ); - console.log(whiteBold("HTTP SSE:") + " " + sseUrlWithKey); console.log( whiteBold("Stdio:"), JSON.stringify(stdioCommand, null, 2), diff --git a/apps/cli/src/commands/core/get.ts b/apps/cli/src/commands/core/get.ts index 01ea84d4..65a58085 100644 --- a/apps/cli/src/commands/core/get.ts +++ b/apps/cli/src/commands/core/get.ts @@ -110,7 +110,6 @@ export function printPlaybookDetails( name, description: description ?? "--", streamableURL: joinURL(env.GATEWAY_URL, playbook.paths.streamable), - sseURL: joinURL(env.GATEWAY_URL, playbook.paths.sse), }), ); diff --git a/apps/cli/src/commands/mcp/prompts.ts b/apps/cli/src/commands/mcp/prompts.ts index 0918f39e..75102c7f 100644 --- a/apps/cli/src/commands/mcp/prompts.ts +++ b/apps/cli/src/commands/mcp/prompts.ts @@ -1,58 +1,34 @@ import { HTTPClient } from "@director.run/mcp/client/http-client"; import { yellow } from "@director.run/utilities/cli/colors"; -import { - DirectorCommand, - makeOption, -} from "@director.run/utilities/cli/director-command"; +import { DirectorCommand } from "@director.run/utilities/cli/director-command"; import { actionWithErrorHandler } from "@director.run/utilities/cli/index"; import { makeTable } from "@director.run/utilities/cli/index"; import type { Prompt } from "@modelcontextprotocol/sdk/types.js"; import { gatewayClient } from "../../client"; import { title } from "../../common"; -import type { TransportType } from "./tools"; /** * Creates an authenticated MCP client for a playbook. */ -async function createPlaybookClient( - playbookId: string, - transport: TransportType = "streamable", -): Promise { +async function createPlaybookClient(playbookId: string): Promise { const connectionInfo = await gatewayClient.store.getConnectionInfo.query({ playbookId, }); // Build URL with API key - const baseUrl = - transport === "sse" ? connectionInfo.sseUrl : connectionInfo.streamableUrl; - const urlWithKey = `${baseUrl}?key=${connectionInfo.apiKey}`; + const urlWithKey = `${connectionInfo.streamableUrl}?key=${connectionInfo.apiKey}`; return HTTPClient.createAndConnectToHTTP(urlWithKey); } -function transportOption() { - return makeOption({ - flags: "--transport ", - description: "Transport type to use for connection", - defaultValue: "streamable", - choices: ["streamable", "sse"], - }); -} - export function registerPromptsCommand(program: DirectorCommand) { program .command("list-prompts ") .description("List prompts on a playbook") - .addOption(transportOption()) .action( - actionWithErrorHandler( - async (playbookId: string, options: { transport: TransportType }) => { - const client = await createPlaybookClient( - playbookId, - options.transport, - ); - await printPrompts(client); - await client.close(); - }, - ), + actionWithErrorHandler(async (playbookId: string) => { + const client = await createPlaybookClient(playbookId); + await printPrompts(client); + await client.close(); + }), ); } diff --git a/apps/cli/src/commands/mcp/tools.ts b/apps/cli/src/commands/mcp/tools.ts index 7c92dafb..42f4c62d 100644 --- a/apps/cli/src/commands/mcp/tools.ts +++ b/apps/cli/src/commands/mcp/tools.ts @@ -1,9 +1,6 @@ import { HTTPClient } from "@director.run/mcp/client/http-client"; import { blue, yellow } from "@director.run/utilities/cli/colors"; -import { - DirectorCommand, - makeOption, -} from "@director.run/utilities/cli/director-command"; +import { DirectorCommand } from "@director.run/utilities/cli/director-command"; import { actionWithErrorHandler } from "@director.run/utilities/cli/index"; import { makeTable } from "@director.run/utilities/cli/index"; import { input } from "@inquirer/prompts"; @@ -11,100 +8,50 @@ import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { gatewayClient } from "../../client"; import { title } from "../../common"; -export type TransportType = "streamable" | "sse"; - /** * Creates an authenticated MCP client for a playbook. */ -async function createPlaybookClient( - playbookId: string, - transport: TransportType = "streamable", -): Promise { +async function createPlaybookClient(playbookId: string): Promise { const connectionInfo = await gatewayClient.store.getConnectionInfo.query({ playbookId, }); // Build URL with API key - const baseUrl = - transport === "sse" ? connectionInfo.sseUrl : connectionInfo.streamableUrl; - const urlWithKey = `${baseUrl}?key=${connectionInfo.apiKey}`; + const urlWithKey = `${connectionInfo.streamableUrl}?key=${connectionInfo.apiKey}`; return HTTPClient.createAndConnectToHTTP(urlWithKey); } -function transportOption() { - return makeOption({ - flags: "--transport ", - description: "Transport type to use for connection", - defaultValue: "streamable", - choices: ["streamable", "sse"], - }); -} - export function registerToolsCommand(program: DirectorCommand) { program .command("list-tools ") .description("List tools on a playbook") - .addOption(transportOption()) .action( - actionWithErrorHandler( - async (playbookId: string, options: { transport: TransportType }) => { - const client = await createPlaybookClient( - playbookId, - options.transport, - ); - await printTools(client); - await client.close(); - }, - ), + actionWithErrorHandler(async (playbookId: string) => { + const client = await createPlaybookClient(playbookId); + await printTools(client); + await client.close(); + }), ); program .command("get-tool ") .description("Get the details of a tool") - .addOption(transportOption()) .action( - actionWithErrorHandler( - async ( - playbookId: string, - toolName: string, - options: { transport: TransportType }, - ) => { - const client = await createPlaybookClient( - playbookId, - options.transport, - ); - await printTool(client, toolName); - await client.close(); - }, - ), + actionWithErrorHandler(async (playbookId: string, toolName: string) => { + const client = await createPlaybookClient(playbookId); + await printTool(client, toolName); + await client.close(); + }), ); program .command("call-tool ") - .addOption( - makeOption({ - flags: "-a,--argument ", - description: - "set arguments in key=value format (can be used multiple times)", - variadic: true, - }), - ) - .addOption(transportOption()) .description("Call a tool on a playbook") .action( - actionWithErrorHandler( - async ( - playbookId: string, - toolName: string, - options: { transport: TransportType }, - ) => { - const client = await createPlaybookClient( - playbookId, - options.transport, - ); - await callTool(client, toolName); - await client.close(); - }, - ), + actionWithErrorHandler(async (playbookId: string, toolName: string) => { + const client = await createPlaybookClient(playbookId); + await callTool(client, toolName); + await client.close(); + }), ); } diff --git a/apps/gateway/src/integration.mcp.proxy.test.ts b/apps/gateway/src/integration.mcp.proxy.test.ts index 060156ac..318e2dea 100644 --- a/apps/gateway/src/integration.mcp.proxy.test.ts +++ b/apps/gateway/src/integration.mcp.proxy.test.ts @@ -21,29 +21,20 @@ import { type GatewayRouterOutputs } from "./client"; import { makePrompt } from "./test/fixtures"; import { IntegrationTestHarness } from "./test/integration"; -enum Transport { - SSE = "sse", - STREAMABLE = "streamable", +function getPlaybookUrl(playbookId: string) { + return `http://localhost:${IntegrationTestHarness.gatewayPort}/playbooks/${playbookId}/mcp`; } -function getPlaybookUrl(transport: Transport, playbookId: string) { - return `http://localhost:${IntegrationTestHarness.gatewayPort}/playbooks/${playbookId}/${transport === Transport.SSE ? "sse" : "mcp"}`; -} - -async function createPlaybookClient( - transport: Transport, - playbookId: string, - apiKey: string, -) { - return await HTTPClient.createAndConnectToHTTP( - getPlaybookUrl(transport, playbookId), - { Authorization: `Bearer ${apiKey}` }, - ); +async function createPlaybookClient(playbookId: string, apiKey: string) { + return await HTTPClient.createAndConnectToHTTP(getPlaybookUrl(playbookId), { + Authorization: `Bearer ${apiKey}`, + }); } describe("MCP Playbook", () => { let harness: IntegrationTestHarness; let playbook: GatewayRouterOutputs["store"]["create"]; + let playbookClient: HTTPClient; beforeAll(async () => { harness = await IntegrationTestHarness.start(); @@ -57,324 +48,309 @@ describe("MCP Playbook", () => { await harness.stop(); }); - [Transport.SSE, Transport.STREAMABLE].forEach((transport) => { - beforeEach(async () => { - await harness.initializeDatabase(true); - playbook = await harness.client.store.create.mutate({ - name: "Test Playbook", - }); - const echoConfig = harness.getConfigForTarget("echo"); - await harness.client.store.addHTTPServer.mutate({ - playbookId: playbook.id, - name: echoConfig.name, - url: echoConfig.transport.url, - }); - const kitchenSinkConfig = harness.getConfigForTarget("kitchenSink"); - await harness.client.store.addHTTPServer.mutate({ - playbookId: playbook.id, - name: kitchenSinkConfig.name, - url: kitchenSinkConfig.transport.url, - }); + beforeEach(async () => { + await harness.initializeDatabase(true); + playbook = await harness.client.store.create.mutate({ + name: "Test Playbook", + }); + const echoConfig = harness.getConfigForTarget("echo"); + await harness.client.store.addHTTPServer.mutate({ + playbookId: playbook.id, + name: echoConfig.name, + url: echoConfig.transport.url, }); + const kitchenSinkConfig = harness.getConfigForTarget("kitchenSink"); + await harness.client.store.addHTTPServer.mutate({ + playbookId: playbook.id, + name: kitchenSinkConfig.name, + url: kitchenSinkConfig.transport.url, + }); + playbookClient = await createPlaybookClient( + playbook.id, + harness.getApiKey(), + ); + }); - describe(`${transport} transport`, () => { - let playbookClient: HTTPClient; + afterEach(async () => { + await playbookClient.close(); + }); - beforeEach(async () => { - playbookClient = await createPlaybookClient( - transport, - playbook.id, - harness.getApiKey(), - ); - }); + it("should return 401 when no API key provided", async () => { + const res = await fetch(getPlaybookUrl("not_existing_playbook")); + expect(res.status).toEqual(401); + expect(res.ok).toBeFalsy(); + }); - afterEach(async () => { - await playbookClient.close(); - }); + it("should return 404 when playbook not found", async () => { + const res = await fetch(getPlaybookUrl("not_existing_playbook"), { + headers: { + Authorization: `Bearer ${harness.getApiKey()}`, + }, + }); + // Returns 404 because the playbook doesn't exist + expect(res.status).toEqual(404); + expect(res.ok).toBeFalsy(); + }); - it("should return 401 when no API key provided", async () => { - const res = await fetch( - getPlaybookUrl(transport, "not_existing_playbook"), - ); - expect(res.status).toEqual(401); - expect(res.ok).toBeFalsy(); + describe("tools", () => { + it("should be able to list tools", async () => { + await expectListToolsToReturnToolNames(playbookClient, [ + "echo", + "ping", + "add", + "subtract", + "multiply", + ]); + }); + + it("should be able to call a tool", async () => { + await expectToolCallToHaveResult({ + client: playbookClient, + toolName: "ping", + arguments: {}, + expectedResult: { message: "pong" }, }); + }); - it("should return 404 when playbook not found", async () => { - const res = await fetch( - getPlaybookUrl(transport, "not_existing_playbook"), - { - headers: { - Authorization: `Bearer ${harness.getApiKey()}`, - }, + describe("tool prefixing", () => { + beforeEach(async () => { + await harness.client.store.updateServer.mutate({ + playbookId: playbook.id, + serverName: "echo", + attributes: { + tools: { prefix: "prefix__" }, }, - ); - // Returns 404 because the playbook doesn't exist - expect(res.status).toEqual(404); - expect(res.ok).toBeFalsy(); + }); }); - describe("tools", () => { - it("should be able to list tools", async () => { - await expectListToolsToReturnToolNames(playbookClient, [ - "echo", - "ping", - "add", - "subtract", - "multiply", - ]); - }); + it("should return prefixed tools in list tools", async () => { + await expectListToolsToReturnToolNames(playbookClient, [ + "prefix__echo", + "ping", + "add", + "subtract", + "multiply", + ]); + }); - it("should be able to call a tool", async () => { - await expectToolCallToHaveResult({ - client: playbookClient, - toolName: "ping", - arguments: {}, - expectedResult: { message: "pong" }, - }); + it("should be able to call a tool with a prefix", async () => { + await expectToolCallToHaveResult({ + client: playbookClient, + toolName: "prefix__echo", + arguments: { message: "Hello" }, + expectedResult: { message: "Hello" }, }); + }); - describe("tool prefixing", () => { - beforeEach(async () => { - await harness.client.store.updateServer.mutate({ - playbookId: playbook.id, - serverName: "echo", - attributes: { - tools: { prefix: "prefix__" }, - }, - }); - }); - - it("should return prefixed tools in list tools", async () => { - await expectListToolsToReturnToolNames(playbookClient, [ - "prefix__echo", - "ping", - "add", - "subtract", - "multiply", - ]); - }); - - it("should be able to call a tool with a prefix", async () => { - await expectToolCallToHaveResult({ - client: playbookClient, - toolName: "prefix__echo", - arguments: { message: "Hello" }, - expectedResult: { message: "Hello" }, - }); - }); - - it("should fail to call the tool without the prefix", async () => { - await expectUnknownToolError({ - client: playbookClient, - toolName: "echo", - arguments: { message: "Hello" }, - }); - }); - - it("should be able to remove the prefix", async () => { - await harness.client.store.updateServer.mutate({ - playbookId: playbook.id, - serverName: "echo", - attributes: { - tools: { prefix: "" }, - }, - }); - - await expectListToolsToReturnToolNames(playbookClient, [ - "echo", - "ping", - "add", - "subtract", - "multiply", - ]); - }); + it("should fail to call the tool without the prefix", async () => { + await expectUnknownToolError({ + client: playbookClient, + toolName: "echo", + arguments: { message: "Hello" }, }); + }); - describe("tool disabling", () => { - beforeEach(async () => { - await harness.client.store.updateServer.mutate({ - playbookId: playbook.id, - serverName: "kitchen-sink", - attributes: { - tools: { exclude: ["ping", "add"] }, - }, - }); - }); - - it("should not return disabled tools in list tools", async () => { - await expectListToolsToReturnToolNames(playbookClient, [ - "echo", - "subtract", - "multiply", - ]); - }); - - it("should fail to call a disabled tool", async () => { - await expectUnknownToolError({ - client: playbookClient, - toolName: "ping", - arguments: {}, - }); - }); - - it("should be able to re-enable a tool", async () => { - await harness.client.store.updateServer.mutate({ - playbookId: playbook.id, - serverName: "kitchen-sink", - attributes: { - tools: { exclude: [] }, - }, - }); - - await expectListToolsToReturnToolNames(playbookClient, [ - "echo", - "ping", - "add", - "subtract", - "multiply", - ]); - }); + it("should be able to remove the prefix", async () => { + await harness.client.store.updateServer.mutate({ + playbookId: playbook.id, + serverName: "echo", + attributes: { + tools: { prefix: "" }, + }, }); + + await expectListToolsToReturnToolNames(playbookClient, [ + "echo", + "ping", + "add", + "subtract", + "multiply", + ]); }); + }); - describe("addHTTPServer", () => { - it("should be able to add a server to a playbook", async () => { - const foobarConfig = harness.getConfigForTarget("foobar"); - await harness.client.store.addHTTPServer.mutate({ - playbookId: playbook.id, - name: foobarConfig.name, - url: foobarConfig.transport.url, - }); - - await expectListToolsToReturnToolNames(playbookClient, [ - "echo", - "ping", - "add", - "subtract", - "multiply", - "foo", - ]); - - await expectToolCallToHaveResult({ - client: playbookClient, - toolName: "foo", - arguments: { - message: "bar", - }, - expectedResult: { message: "bar" }, - }); + describe("tool disabling", () => { + beforeEach(async () => { + await harness.client.store.updateServer.mutate({ + playbookId: playbook.id, + serverName: "kitchen-sink", + attributes: { + tools: { exclude: ["ping", "add"] }, + }, }); }); - describe("removeServer", () => { - it("should be able to remove a server from a playbook", async () => { - await harness.client.store.removeServer.mutate({ - playbookId: playbook.id, - serverName: harness.getConfigForTarget("kitchenSink").name, - }); - - await expectListToolsToReturnToolNames(playbookClient, ["echo"]); - await expectUnknownToolError({ - client: playbookClient, - toolName: "ping", - arguments: {}, - }); - }); + it("should not return disabled tools in list tools", async () => { + await expectListToolsToReturnToolNames(playbookClient, [ + "echo", + "subtract", + "multiply", + ]); }); - describe("disabling targets", () => { - beforeEach(async () => { - await harness.client.store.updateServer.mutate({ - playbookId: playbook.id, - serverName: "kitchen-sink", - attributes: { disabled: true }, - }); + it("should fail to call a disabled tool", async () => { + await expectUnknownToolError({ + client: playbookClient, + toolName: "ping", + arguments: {}, }); + }); - it("should not return tools in list tools on a disabled target", async () => { - await expectListToolsToReturnToolNames(playbookClient, ["echo"]); + it("should be able to re-enable a tool", async () => { + await harness.client.store.updateServer.mutate({ + playbookId: playbook.id, + serverName: "kitchen-sink", + attributes: { + tools: { exclude: [] }, + }, }); - it("should fail to call tools on a disabled target", async () => { - await expectUnknownToolError({ - client: playbookClient, - toolName: "ping", - arguments: {}, - }); - }); + await expectListToolsToReturnToolNames(playbookClient, [ + "echo", + "ping", + "add", + "subtract", + "multiply", + ]); }); + }); + }); - describe("prompts", () => { - const prompt = makePrompt(); + describe("addHTTPServer", () => { + it("should be able to add a server to a playbook", async () => { + const foobarConfig = harness.getConfigForTarget("foobar"); + await harness.client.store.addHTTPServer.mutate({ + playbookId: playbook.id, + name: foobarConfig.name, + url: foobarConfig.transport.url, + }); - beforeEach(async () => { - await harness.client.store.addPrompt.mutate({ - playbookId: playbook.id, - prompt, - }); - }); + await expectListToolsToReturnToolNames(playbookClient, [ + "echo", + "ping", + "add", + "subtract", + "multiply", + "foo", + ]); + + await expectToolCallToHaveResult({ + client: playbookClient, + toolName: "foo", + arguments: { + message: "bar", + }, + expectedResult: { message: "bar" }, + }); + }); + }); - it("should return the prompt", async () => { - await expectGetPromptToReturn({ - client: playbookClient, - promptName: prompt.name, - expectedBody: prompt.body, - }); - }); + describe("removeServer", () => { + it("should be able to remove a server from a playbook", async () => { + await harness.client.store.removeServer.mutate({ + playbookId: playbook.id, + serverName: harness.getConfigForTarget("kitchenSink").name, + }); - it("should be able to list prompts", async () => { - await expectListPromptsToReturn({ - client: playbookClient, - expectedPrompts: [ - { - name: prompt.name, - title: prompt.title, - description: prompt.description, - }, - ], - }); - }); + await expectListToolsToReturnToolNames(playbookClient, ["echo"]); + await expectUnknownToolError({ + client: playbookClient, + toolName: "ping", + arguments: {}, + }); + }); + }); - it("should be able to update a prompt", async () => { - await harness.client.store.updatePrompt.mutate({ - playbookId: playbook.id, - promptName: prompt.name, - prompt: { - title: "Updated Title", - description: "Updated description", - body: "Updated body", - }, - }); - - await expectGetPromptToReturn({ - client: playbookClient, - promptName: prompt.name, - expectedBody: "Updated body", - }); - - await expectListPromptsToReturn({ - client: playbookClient, - expectedPrompts: [ - { - name: prompt.name, - title: "Updated Title", - description: "Updated description", - }, - ], - }); - }); + describe("disabling targets", () => { + beforeEach(async () => { + await harness.client.store.updateServer.mutate({ + playbookId: playbook.id, + serverName: "kitchen-sink", + attributes: { disabled: true }, + }); + }); - it("should be able to remove a prompt", async () => { - await harness.client.store.removePrompt.mutate({ - playbookId: playbook.id, - promptName: prompt.name, - }); - await expectListPromptsToReturn({ - client: playbookClient, - expectedPrompts: [], - }); - }); + it("should not return tools in list tools on a disabled target", async () => { + await expectListToolsToReturnToolNames(playbookClient, ["echo"]); + }); + + it("should fail to call tools on a disabled target", async () => { + await expectUnknownToolError({ + client: playbookClient, + toolName: "ping", + arguments: {}, + }); + }); + }); + + describe("prompts", () => { + const prompt = makePrompt(); + + beforeEach(async () => { + await harness.client.store.addPrompt.mutate({ + playbookId: playbook.id, + prompt, + }); + }); + + it("should return the prompt", async () => { + await expectGetPromptToReturn({ + client: playbookClient, + promptName: prompt.name, + expectedBody: prompt.body, + }); + }); + + it("should be able to list prompts", async () => { + await expectListPromptsToReturn({ + client: playbookClient, + expectedPrompts: [ + { + name: prompt.name, + title: prompt.title, + description: prompt.description, + }, + ], + }); + }); + + it("should be able to update a prompt", async () => { + await harness.client.store.updatePrompt.mutate({ + playbookId: playbook.id, + promptName: prompt.name, + prompt: { + title: "Updated Title", + description: "Updated description", + body: "Updated body", + }, + }); + + await expectGetPromptToReturn({ + client: playbookClient, + promptName: prompt.name, + expectedBody: "Updated body", + }); + + await expectListPromptsToReturn({ + client: playbookClient, + expectedPrompts: [ + { + name: prompt.name, + title: "Updated Title", + description: "Updated description", + }, + ], + }); + }); + + it("should be able to remove a prompt", async () => { + await harness.client.store.removePrompt.mutate({ + playbookId: playbook.id, + promptName: prompt.name, + }); + await expectListPromptsToReturn({ + client: playbookClient, + expectedPrompts: [], }); }); }); diff --git a/apps/gateway/src/playbooks/playbook-schema.ts b/apps/gateway/src/playbooks/playbook-schema.ts index 650db305..d69d89fc 100644 --- a/apps/gateway/src/playbooks/playbook-schema.ts +++ b/apps/gateway/src/playbooks/playbook-schema.ts @@ -47,6 +47,5 @@ export type PlaybookPlainObject = Omit & { servers: (HTTPClientPlainObject | StdioClientPlainObject)[]; paths: { streamable: string; - sse: string; }; }; diff --git a/apps/gateway/src/playbooks/playbook.ts b/apps/gateway/src/playbooks/playbook.ts index a0e2d36a..e03740e7 100644 --- a/apps/gateway/src/playbooks/playbook.ts +++ b/apps/gateway/src/playbooks/playbook.ts @@ -94,10 +94,6 @@ export class Playbook extends ProxyServer { return `/playbooks/${this.id}/mcp`; } - get ssePath() { - return `/playbooks/${this.id}/sse`; - } - public async addTarget( server: PlaybookTarget | ProxyTarget, params: { throwOnError: boolean } = { throwOnError: true }, @@ -333,7 +329,6 @@ export class Playbook extends ProxyServer { ), paths: { streamable: this.streamablePath, - sse: this.ssePath, }, }; } diff --git a/apps/gateway/src/routers/mcp/mcp.ts b/apps/gateway/src/routers/mcp/mcp.ts index e6ff1993..89270a64 100644 --- a/apps/gateway/src/routers/mcp/mcp.ts +++ b/apps/gateway/src/routers/mcp/mcp.ts @@ -3,7 +3,6 @@ import type { Database } from "../../db/database"; import { requireAPIKeyAuth } from "../../middleware/auth"; import type { PlaybookStore } from "../../playbooks/playbook-store"; import { createMcpNextRouter } from "./mcp-next"; -import { createSSERouter } from "./sse"; import { createStreamableRouter } from "./streamable"; export function createMCPRouter({ @@ -29,12 +28,6 @@ export function createMCPRouter({ // API key authentication for legacy endpoints router.use(requireAPIKeyAuth()); - router.use( - createSSERouter({ - playbookStore, - }), - ); - router.use( createStreamableRouter({ playbookStore, diff --git a/apps/gateway/src/routers/mcp/sse.ts b/apps/gateway/src/routers/mcp/sse.ts deleted file mode 100644 index 5149853c..00000000 --- a/apps/gateway/src/routers/mcp/sse.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { ErrorCode } from "@director.run/utilities/error"; -import { AppError } from "@director.run/utilities/error"; -import { getLogger } from "@director.run/utilities/logger"; -import { asyncHandler } from "@director.run/utilities/middleware/index"; -import { Telemetry } from "@director.run/utilities/telemetry"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import express from "express"; -import { - type AuthenticatedRequest, - // requireAPIKeyAuth, -} from "../../middleware/auth"; -import type { PlaybookStore } from "../../playbooks/playbook-store"; - -const logger = getLogger("mcp/sse"); - -export const createSSERouter = ({ - playbookStore, - telemetry, -}: { - playbookStore: PlaybookStore; - telemetry?: Telemetry; -}): express.Router => { - const router = express.Router(); - const transports: Map = new Map(); - - router.get( - "/:playbook_id/sse", - asyncHandler(async (req, res) => { - const authReq = req as AuthenticatedRequest; - - const playbookId = req.params.playbook_id; - const userId = authReq.userId; - const playbook = await playbookStore.get(playbookId, userId); - const transport = new SSEServerTransport( - // TODO: Make this configurable. - `playbooks/${playbook.id}/message`, - res, - ); - - transports.set(transport.sessionId, transport); - - logger.info({ - message: "SSE connection started", - sessionId: transport.sessionId, - playbookId: playbook.id, - userAgent: req.headers["user-agent"], - host: req.headers["host"], - }); - - telemetry?.trackEvent("connection_started", { - transport: "sse", - }); - /** - * The MCP documentation says to use res.on("close", () => { ... }) to - * clean up the transport when the connection is closed. However, this - * doesn't work for some reason. So we use this instead. - * - * [TODO] Figure out if this is correct. Also add a test case for this. - */ - req.socket.on("close", () => { - logger.info({ - message: "SSE connection closed", - sessionId: transport.sessionId, - playbookId: playbook.id, - }); - transports.delete(transport.sessionId); - }); - - await playbook.connect(transport); - }), - ); - - router.post( - "/:playbook_id/message", - asyncHandler(async (req, res) => { - const playbookId = req.params.playbook_id; - const authReq = req as AuthenticatedRequest; - const userId = authReq.userId; - const playbook = await playbookStore.get(playbookId, userId); - const sessionId = req.query.sessionId?.toString(); - - if (!sessionId) { - // TODO: Add a test case for this. - throw new AppError(ErrorCode.BAD_REQUEST, "No sessionId provided"); - } - - // Body is already parsed by express.json() middleware at the parent router level - const body = req.body; - - logger.info({ - message: "Message received", - playbookId: playbook.id, - sessionId, - method: body.method, - params: body.params, - }); - - const transport = transports.get(sessionId); - - if (!transport) { - // TODO: Add a test case for this. - logger.warn({ - message: "Transport not found", - sessionId, - playbookId: playbook.id, - }); - throw new AppError(ErrorCode.NOT_FOUND, "Transport not found"); - } - - telemetry?.trackEvent("method_called", { - method: body.method, - transport: "sse", - }); - - await transport.handlePostMessage(req, res, body); - }), - ); - - return router; -}; diff --git a/apps/gateway/src/routers/trpc/store-router.ts b/apps/gateway/src/routers/trpc/store-router.ts index fa7d8de2..a03a59fa 100644 --- a/apps/gateway/src/routers/trpc/store-router.ts +++ b/apps/gateway/src/routers/trpc/store-router.ts @@ -583,13 +583,11 @@ export function createPlaybookStoreRouter() { // Build URLs without API key (key returned separately for obfuscation) const streamableUrl = joinURL(env.BASE_URL, playbook.streamablePath); - const sseUrl = joinURL(env.BASE_URL, playbook.ssePath); return { playbookId, apiKey, streamableUrl, - sseUrl, }; }), }); diff --git a/apps/studio/src/components/sidebars/registry-item-add-form.stories.tsx b/apps/studio/src/components/sidebars/registry-item-add-form.stories.tsx index 27e84676..6a95bd09 100644 --- a/apps/studio/src/components/sidebars/registry-item-add-form.stories.tsx +++ b/apps/studio/src/components/sidebars/registry-item-add-form.stories.tsx @@ -21,7 +21,6 @@ const mockPlaybooks: PlaybookList = [ servers: [], paths: { streamable: "/ws/dev-playbook", - sse: "/ws/dev-playbook", }, }, { @@ -33,7 +32,6 @@ const mockPlaybooks: PlaybookList = [ servers: [], paths: { streamable: "/ws/staging-playbook", - sse: "/ws/staging-playbook", }, }, { @@ -45,7 +43,6 @@ const mockPlaybooks: PlaybookList = [ servers: [], paths: { streamable: "/ws/production-playbook", - sse: "/ws/production-playbook", }, }, ]; diff --git a/apps/studio/src/config.ts b/apps/studio/src/config.ts index 6d5032c3..b3160f0b 100644 --- a/apps/studio/src/config.ts +++ b/apps/studio/src/config.ts @@ -7,3 +7,7 @@ export const GATEWAY_URL: string = export const REGISTRY_URL: string = appConfig?.registryUrl || "https://registry.director.run"; export const BASE_PATH: string = appConfig?.basePath || "/"; +export const DANGEROUSLY_PREFILL_LOGIN_EMAIL: string | undefined = + process.env.NODE_ENV === "development" ? "user@director.run" : ""; +export const DANGEROUSLY_PREFILL_LOGIN_PASSWORD: string | undefined = + process.env.NODE_ENV === "development" ? "password" : ""; diff --git a/apps/studio/src/pages/connect-page.tsx b/apps/studio/src/pages/connect-page.tsx index 161d3f14..ef31ad6f 100644 --- a/apps/studio/src/pages/connect-page.tsx +++ b/apps/studio/src/pages/connect-page.tsx @@ -1,10 +1,13 @@ import { ConnectPage as ConnectPageComponent } from "@director.run/design/components/pages/auth/connect.tsx"; import { useCallback, useState } from "react"; import { Link, useSearchParams } from "react-router-dom"; +import { + DANGEROUSLY_PREFILL_LOGIN_EMAIL, + DANGEROUSLY_PREFILL_LOGIN_PASSWORD, +} from "../config.ts"; import { useAuth } from "../contexts/auth-context.tsx"; import { gatewayClient } from "../contexts/backend-context.tsx"; import { authClient } from "../lib/auth-client.ts"; - /** * OAuth consent page for MCP clients. * @@ -134,7 +137,10 @@ export function ConnectPage() { redirectUri={consentInfoQuery.data?.redirectUri || null} onApprove={handleApprove} onDeny={handleDeny} - defaultValues={{ email: "", password: "" }} + defaultValues={{ + email: DANGEROUSLY_PREFILL_LOGIN_EMAIL || "", + password: DANGEROUSLY_PREFILL_LOGIN_PASSWORD || "", + }} onLogin={handleLogin} signupLink={ diff --git a/apps/studio/src/pages/login-page.tsx b/apps/studio/src/pages/login-page.tsx index 6a362cbc..be64453b 100644 --- a/apps/studio/src/pages/login-page.tsx +++ b/apps/studio/src/pages/login-page.tsx @@ -1,6 +1,10 @@ import { LoginPage as LoginPageComponent } from "@director.run/design/components/pages/auth/login.tsx"; import { useState } from "react"; import { Link, Navigate, useNavigate } from "react-router-dom"; +import { + DANGEROUSLY_PREFILL_LOGIN_EMAIL, + DANGEROUSLY_PREFILL_LOGIN_PASSWORD, +} from "../config.ts"; import { useAuth } from "../contexts/auth-context.tsx"; export function LoginPage() { @@ -18,7 +22,10 @@ export function LoginPage() { return ( { try { setError(null); diff --git a/apps/studio/src/pages/onboarding.stories.tsx b/apps/studio/src/pages/onboarding.stories.tsx index 8967cdc3..b6c2baaa 100644 --- a/apps/studio/src/pages/onboarding.stories.tsx +++ b/apps/studio/src/pages/onboarding.stories.tsx @@ -11,7 +11,6 @@ const mockConnectionInfo: ConnectionInfo = { playbookId: "playbook-1", apiKey: "sk_test_abc123def456ghi789", streamableUrl: "https://gateway.director.run/mcp/playbook-1", - sseUrl: "https://gateway.director.run/sse/playbook-1", }; const meta = { diff --git a/apps/studio/src/pages/playbook-detail.stories.tsx b/apps/studio/src/pages/playbook-detail.stories.tsx index 55419f39..f81706c1 100644 --- a/apps/studio/src/pages/playbook-detail.stories.tsx +++ b/apps/studio/src/pages/playbook-detail.stories.tsx @@ -30,7 +30,6 @@ const mockConnectionInfo: ConnectionInfo = { playbookId: "staging-playbook", apiKey: "sk_test_abc123def456ghi789", streamableUrl: "https://gateway.director.run/mcp/staging-playbook", - sseUrl: "https://gateway.director.run/sse/staging-playbook", }; const PlaybookDetailComponent = ({ diff --git a/apps/studio/src/pages/registry-item-detail.stories.tsx b/apps/studio/src/pages/registry-item-detail.stories.tsx index 1483d6db..27cc3dea 100644 --- a/apps/studio/src/pages/registry-item-detail.stories.tsx +++ b/apps/studio/src/pages/registry-item-detail.stories.tsx @@ -21,7 +21,6 @@ const mockPlaybooks: PlaybookList = [ servers: [], paths: { streamable: "/ws/dev-playbook", - sse: "/ws/dev-playbook", }, }, { @@ -33,7 +32,6 @@ const mockPlaybooks: PlaybookList = [ servers: [], paths: { streamable: "/ws/staging-playbook", - sse: "/ws/staging-playbook", }, }, ]; diff --git a/packages/client-configurator/src/client-store.ts b/packages/client-configurator/src/client-store.ts index 5848f03d..20d983c0 100644 --- a/packages/client-configurator/src/client-store.ts +++ b/packages/client-configurator/src/client-store.ts @@ -8,7 +8,6 @@ import { AppError, ErrorCode } from "@director.run/utilities/error"; export type ClientId = "claude" | "claude-code" | "cursor" | "vscode"; export type ConnectionDetails = { - sseUrl: string; streamableUrl: string; }; @@ -75,7 +74,6 @@ export class ClientStore { const result = await client.install({ name, - sseURL: connectionDetails.sseUrl, streamableURL: connectionDetails.streamableUrl, }); diff --git a/packages/client-configurator/src/integration.test.ts b/packages/client-configurator/src/integration.test.ts index 51a4cc2c..c836e959 100644 --- a/packages/client-configurator/src/integration.test.ts +++ b/packages/client-configurator/src/integration.test.ts @@ -60,7 +60,6 @@ import type { VSCodeConfig } from "./vscode"; entries: [ { name: "not_managed_by_director", - sseURL: faker.internet.url(), streamableURL: faker.internet.url(), }, ], diff --git a/packages/client-configurator/src/test/fixtures.ts b/packages/client-configurator/src/test/fixtures.ts index f19fb43a..61d8b5dd 100644 --- a/packages/client-configurator/src/test/fixtures.ts +++ b/packages/client-configurator/src/test/fixtures.ts @@ -24,7 +24,7 @@ export function createVSCodeConfig(entries: Array): VSCodeConfig { mcp: { servers: entries.reduce( (acc, entry) => { - acc[entry.name] = { url: entry.sseURL }; + acc[entry.name] = { url: entry.streamableURL }; return acc; }, {} as Record, @@ -37,7 +37,7 @@ export function createCursorConfig(entries: Array): CursorConfig { return { mcpServers: entries.reduce( (acc, entry) => { - acc[entry.name] = { url: entry.sseURL }; + acc[entry.name] = { url: entry.streamableURL }; return acc; }, {} as Record, @@ -51,7 +51,12 @@ export function createClaudeConfig(entries: Array): ClaudeConfig { (acc, entry) => { acc[entry.name] = { command: "npx", - args: ["-y", "@director.run/cli@latest", "http2stdio", entry.sseURL], + args: [ + "-y", + "@director.run/cli@latest", + "http2stdio", + entry.streamableURL, + ], env: { LOG_LEVEL: "silent", }, @@ -78,12 +83,10 @@ export function createClaudeCodeConfig( } export function createInstallable(): { - sseURL: string; name: string; streamableURL: string; } { return { - sseURL: faker.internet.url(), name: [faker.hacker.noun(), faker.string.uuid()].join("-"), streamableURL: faker.internet.url(), }; diff --git a/packages/client-configurator/src/types.ts b/packages/client-configurator/src/types.ts index 4fcab85d..ec4c2df9 100644 --- a/packages/client-configurator/src/types.ts +++ b/packages/client-configurator/src/types.ts @@ -130,6 +130,5 @@ export abstract class AbstractClient { export type Installable = { name: string; - sseURL: string; streamableURL: string; }; diff --git a/packages/client-configurator/src/vscode.ts b/packages/client-configurator/src/vscode.ts index d2bdf061..253c1251 100644 --- a/packages/client-configurator/src/vscode.ts +++ b/packages/client-configurator/src/vscode.ts @@ -109,7 +109,7 @@ export class VSCodeInstaller extends AbstractClient { }, }; newConfig.mcp.servers[this.createServerConfigKey(entry.name)] = { - url: entry.sseURL, + url: entry.streamableURL, }; await this.updateConfig(newConfig); return { diff --git a/packages/design/src/components/playbooks-clients/playbook-section-connect.tsx b/packages/design/src/components/playbooks-clients/playbook-section-connect.tsx index 409a7873..7bceca98 100644 --- a/packages/design/src/components/playbooks-clients/playbook-section-connect.tsx +++ b/packages/design/src/components/playbooks-clients/playbook-section-connect.tsx @@ -13,7 +13,6 @@ export interface ConnectionInfo { playbookId: string; apiKey: string; streamableUrl: string; - sseUrl: string; } export interface PlaybookSectionConnectProps {