Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@slack/web-api": "7.15.0",
"@superset/auth": "workspace:*",
"@superset/db": "workspace:*",
"@superset/mcp-v2": "workspace:*",
"@superset/mcp": "workspace:*",
"@superset/shared": "workspace:*",
"@superset/trpc": "workspace:*",
"@t3-oss/env-nextjs": "0.13.11",
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/.well-known/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function GET(request: Request): Response {
name: "superset",
description:
"Superset MCP server — orchestrate parallel coding agents, workspaces, automations, and tasks.",
url: `${origin}/api/v2/agent/mcp`,
url: `${origin}/mcp`,
transport: "streamable-http",
serverCard: `${origin}/.well-known/mcp/server-card.json`,
authentication: {
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/.well-known/mcp/server-card.json/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createMcpServer } from "@superset/mcp-v2";
import { createMcpServer } from "@superset/mcp";
import { getRequestOrigin } from "@/lib/oauth-metadata";

async function listTools() {
Expand Down Expand Up @@ -32,7 +32,7 @@ export async function GET(request: Request): Promise<Response> {
description:
"Superset MCP server: create Git-worktree workspaces, launch coding-agent sessions, schedule automations, open terminals, and manage tasks on behalf of a Superset user.",
version: "0.1.0",
serverUrl: `${origin}/api/v2/agent/mcp`,
serverUrl: `${origin}/mcp`,
transport: "streamable-http",
documentationUrl: "https://docs.superset.sh/mcp-server",
authentication: {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/api/[...unmatched]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function notFound(request: Request): Response {
error: {
code: "NOT_FOUND",
message: `No route matches ${new URL(request.url).pathname}.`,
hint: `API surface: ${origin}/openapi.json. MCP server: ${origin}/api/v2/agent/mcp. Auth: https://superset.sh/auth.md`,
hint: `API surface: ${origin}/openapi.json. MCP server: ${origin}/mcp. Auth: https://superset.sh/auth.md`,
},
},
{ status: 404 },
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/api/agent/[transport]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function gone(): Response {
error: {
code: "GONE",
message: "The v1 MCP server has been removed.",
hint: `Connect to ${env.NEXT_PUBLIC_API_URL}/api/v2/agent/mcp instead.`,
hint: `Connect to ${env.NEXT_PUBLIC_API_URL}/mcp instead.`,
},
},
{ status: 410 },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type Anthropic from "@anthropic-ai/sdk";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createInMemoryMcpClient } from "@superset/mcp-v2/in-memory";
import { createInMemoryMcpClient } from "@superset/mcp/in-memory";
import { posthog } from "@/lib/analytics";
import { getRelayUrl } from "@/lib/relay-url";

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/api/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function unauthorized(request: Request): Response {
error: {
code: "UNAUTHORIZED",
message: "Authentication required.",
hint: `Authenticate via OAuth 2.1 (see https://superset.sh/auth.md) or a Superset API key, then use the MCP server at ${origin}/api/v2/agent/mcp. API surface: ${origin}/openapi.json`,
hint: `Authenticate via OAuth 2.1 (see https://superset.sh/auth.md) or a Superset API key, then use the MCP server at ${origin}/mcp. API surface: ${origin}/openapi.json`,
},
},
{
Expand Down
78 changes: 2 additions & 76 deletions apps/api/src/app/api/v2/agent/[transport]/route.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,4 @@
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import {
createMcpServer,
isMcpUnauthorized,
MCP_SERVER_VERSION,
type McpContext,
resolveMcpContext,
} from "@superset/mcp-v2";
import { env } from "@/env";
import { posthog } from "@/lib/analytics";
import { getOAuthProtectedResourceMetadataUrl } from "@/lib/oauth-metadata";
import { getRelayUrl } from "@/lib/relay-url";

function unauthorizedResponse(req: Request, message: string): Response {
return new Response(
JSON.stringify({ error: { code: "UNAUTHORIZED", message } }),
{
status: 401,
headers: {
"WWW-Authenticate": `Bearer realm="superset", resource_metadata="${getOAuthProtectedResourceMetadataUrl(req)}"`,
"Content-Type": "application/json",
},
},
);
}

async function handle(req: Request): Promise<Response> {
let ctx: McpContext;
try {
ctx = await resolveMcpContext(req, {
apiUrl: env.NEXT_PUBLIC_API_URL,
relayUrl: env.RELAY_URL,
});
} catch (error) {
if (isMcpUnauthorized(error)) {
return unauthorizedResponse(req, error.message);
}
throw error;
}

ctx.relayUrl = await getRelayUrl(ctx.userId);

const server = createMcpServer({
onToolCall: (event) => {
posthog.capture({
distinctId: event.userId,
event: "mcp_tool_called",
properties: {
tool: event.toolName,
organization_id: event.organizationId,
auth_source: event.source,
client_label: event.clientLabel,
duration_ms: event.durationMs,
success: event.success,
error_message: event.errorMessage,
mcp_server: "superset-v2",
mcp_server_version: MCP_SERVER_VERSION,
},
groups: { organization: event.organizationId },
});
},
});
const transport = new WebStandardStreamableHTTPServerTransport();
await server.connect(transport);

return transport.handleRequest(req, {
authInfo: {
token: ctx.bearerToken,
clientId: ctx.source === "api-key" ? "api-key" : "oauth",
scopes: ["mcp:full"],
extra: { mcpContext: ctx },
},
});
}
// Legacy alias kept for already-configured clients. Canonical: /mcp.
export { DELETE, GET, POST } from "../../../../mcp/route";

export const maxDuration = 800;

export { handle as GET, handle as POST, handle as DELETE };
78 changes: 76 additions & 2 deletions apps/api/src/app/mcp/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,78 @@
// Alias: /mcp is the memorable URL for the v2 MCP server.
export { DELETE, GET, POST } from "../api/v2/agent/[transport]/route";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import {
createMcpServer,
isMcpUnauthorized,
MCP_SERVER_VERSION,
type McpContext,
resolveMcpContext,
} from "@superset/mcp";
import { env } from "@/env";
import { posthog } from "@/lib/analytics";
import { getOAuthProtectedResourceMetadataUrl } from "@/lib/oauth-metadata";
import { getRelayUrl } from "@/lib/relay-url";

function unauthorizedResponse(req: Request, message: string): Response {
return new Response(
JSON.stringify({ error: { code: "UNAUTHORIZED", message } }),
{
status: 401,
headers: {
"WWW-Authenticate": `Bearer realm="superset", resource_metadata="${getOAuthProtectedResourceMetadataUrl(req)}"`,
"Content-Type": "application/json",
},
},
);
}

async function handle(req: Request): Promise<Response> {
let ctx: McpContext;
try {
ctx = await resolveMcpContext(req, {
apiUrl: env.NEXT_PUBLIC_API_URL,
relayUrl: env.RELAY_URL,
});
} catch (error) {
if (isMcpUnauthorized(error)) {
return unauthorizedResponse(req, error.message);
}
throw error;
}

ctx.relayUrl = await getRelayUrl(ctx.userId);

const server = createMcpServer({
onToolCall: (event) => {
posthog.capture({
distinctId: event.userId,
event: "mcp_tool_called",
properties: {
tool: event.toolName,
organization_id: event.organizationId,
auth_source: event.source,
client_label: event.clientLabel,
duration_ms: event.durationMs,
success: event.success,
error_message: event.errorMessage,
mcp_server: "superset-v2",
mcp_server_version: MCP_SERVER_VERSION,
},
groups: { organization: event.organizationId },
});
},
});
const transport = new WebStandardStreamableHTTPServerTransport();
await server.connect(transport);

return transport.handleRequest(req, {
authInfo: {
token: ctx.bearerToken,
clientId: ctx.source === "api-key" ? "api-key" : "oauth",
scopes: ["mcp:full"],
extra: { mcpContext: ctx },
},
});
}

export const maxDuration = 800;

export { handle as GET, handle as POST, handle as DELETE };
6 changes: 3 additions & 3 deletions apps/api/src/app/openapi.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const SPEC = {
description: [
"Superset (https://superset.sh) runs parallel AI coding agents in isolated Git worktrees.",
"",
"The primary programmatic surface is the **MCP server** (Model Context Protocol, JSON-RPC 2.0 over Streamable HTTP) at `/api/v2/agent/mcp`. It exposes tools for tasks, workspaces, coding-agent sessions, terminals, automations, hosts, projects, and organization members. The tool catalog with input schemas is published at `" +
"The primary programmatic surface is the **MCP server** (Model Context Protocol, JSON-RPC 2.0 over Streamable HTTP) at `/mcp` (legacy alias: `/api/v2/agent/mcp`). It exposes tools for tasks, workspaces, coding-agent sessions, terminals, automations, hosts, projects, and organization members. The tool catalog with input schemas is published at `" +
`${API_URL}/.well-known/mcp/server-card.json` +
"` and served live via the MCP `tools/list` method.",
"",
Expand All @@ -123,13 +123,13 @@ const SPEC = {
],
security: [{ bearerAuth: [] }],
paths: {
"/api/v2/agent/mcp": {
"/mcp": {
post: {
operationId: "mcpRequest",
tags: ["mcp"],
summary: "Send an MCP JSON-RPC request",
description:
"Streamable HTTP transport endpoint for the Superset MCP server. Send `initialize`, then `tools/list` to enumerate the available tools, then `tools/call` to act on the authenticated user's tasks, workspaces, agents, automations, terminals, hosts, and projects. Responses are `application/json` or `text/event-stream` depending on the request's Accept header.",
"Streamable HTTP transport endpoint for the Superset MCP server (also served at the legacy alias /api/v2/agent/mcp). Send `initialize`, then `tools/list` to enumerate the available tools, then `tools/call` to act on the authenticated user's tasks, workspaces, agents, automations, terminals, hosts, and projects. Responses are `application/json` or `text/event-stream` depending on the request's Accept header.",
requestBody: {
required: true,
content: {
Expand Down
Loading
Loading