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
13 changes: 7 additions & 6 deletions app/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { type ApiKeyAuthResult, authenticateApiKey } from "@/lib/api-key-auth";
import { McpEventStore } from "@/lib/mcp/event-store";
import { getInternalApiBaseUrl } from "@/lib/mcp/internal-url";
import { logMcpEvent } from "@/lib/mcp/logging";
import { authenticateOAuthToken } from "@/lib/mcp/oauth-auth";
import { checkMcpRateLimit } from "@/lib/mcp/rate-limit";
Expand Down Expand Up @@ -177,7 +178,7 @@ function buildSession(
organizationId: string,
apiKeyId: string,
scope: string | undefined,
baseUrl: string,
internalApiBaseUrl: string,
authHeader: string
): BuiltSession {
const eventStore = new McpEventStore();
Expand All @@ -197,7 +198,7 @@ function buildSession(
enableJsonResponse: true,
});

const server = createMcpServer(baseUrl, authHeader, scope);
const server = createMcpServer(internalApiBaseUrl, authHeader, scope);

const entry: SessionEntry = {
transport,
Expand Down Expand Up @@ -278,7 +279,7 @@ async function resolveSession(
orgId: organizationId,
});

const baseUrl = getBaseUrl(request);
const internalApiBaseUrl = getInternalApiBaseUrl();
// Re-derive the auth header from the current request so tool calls in this
// reconstructed session use the caller's credentials.
const authHeader = request.headers.get("authorization") ?? "";
Expand All @@ -287,7 +288,7 @@ async function resolveSession(
organizationId,
result.payload.key,
result.payload.scope,
baseUrl,
internalApiBaseUrl,
authHeader
);

Expand Down Expand Up @@ -483,14 +484,14 @@ export async function POST(request: Request): Promise<Response> {
scope,
});

const baseUrl = getBaseUrl(request);
const internalApiBaseUrl = getInternalApiBaseUrl();
const authHeader = request.headers.get("authorization") ?? "";
const { transport, entry } = buildSession(
newSessionId,
organizationId,
apiKeyId,
scope,
baseUrl,
internalApiBaseUrl,
authHeader
);

Expand Down
13 changes: 7 additions & 6 deletions app/mcp/w/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { type ApiKeyAuthResult, authenticateApiKey } from "@/lib/api-key-auth";
import { McpEventStore } from "@/lib/mcp/event-store";
import { getInternalApiBaseUrl } from "@/lib/mcp/internal-url";
import { getWorkflowListing } from "@/lib/mcp/listing";
import { logMcpEvent } from "@/lib/mcp/logging";
import { authenticateOAuthToken } from "@/lib/mcp/oauth-auth";
Expand Down Expand Up @@ -157,7 +158,7 @@ function buildSession(
organizationId: string,
apiKeyId: string,
scope: string | undefined,
baseUrl: string,
internalApiBaseUrl: string,
authHeader: string,
slug: string,
listing: Parameters<typeof createWorkflowMcpServer>[0]["listing"]
Expand All @@ -179,7 +180,7 @@ function buildSession(
const server = createWorkflowMcpServer({
slug,
listing,
baseUrl,
internalApiBaseUrl,
authHeader,
scope,
});
Expand Down Expand Up @@ -261,14 +262,14 @@ async function resolveSession(
orgId: organizationId,
});

const baseUrl = getBaseUrl(request);
const internalApiBaseUrl = getInternalApiBaseUrl();
const authHeader = request.headers.get("authorization") ?? "";
const { transport, entry } = buildSession(
sessionId,
organizationId,
result.payload.key,
result.payload.scope,
baseUrl,
internalApiBaseUrl,
authHeader,
slug,
listing
Expand Down Expand Up @@ -440,14 +441,14 @@ export async function POST(
scope,
});

const baseUrl = getBaseUrl(request);
const internalApiBaseUrl = getInternalApiBaseUrl();
const authHeader = request.headers.get("authorization") ?? "";
const { transport, entry } = buildSession(
newSessionId,
organizationId,
apiKeyId,
scope,
baseUrl,
internalApiBaseUrl,
authHeader,
slug,
listing
Expand Down
2 changes: 1 addition & 1 deletion keeperhub-events/event-tracker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Base stage - common setup
# ==============================================================================
FROM node:22-alpine AS base
RUN npm install -g pnpm
RUN npm install -g pnpm@10
WORKDIR /app

# ==============================================================================
Expand Down
23 changes: 23 additions & 0 deletions lib/mcp/internal-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const TRAILING_SLASH = /\/+$/;

/**
* Base URL for the MCP server's server-to-server calls into this app's own
* /api/* routes (execution, workflow invocation, status polling, resources).
*
* This MUST stay on the in-pod loopback. The MCP route handlers and the /api
* routes run in the same Next.js process, so routing these internal calls
* through the public hostname (app.keeperhub.com) sends them out to the
* Cloudflare edge — whose managed WAF challenges the pod-to-edge request and
* 403s tool execution. Never substitute the public / OAuth base URL here;
* that one is for metadata documents the client must reach, not for our own
* internal fetches.
*
* INTERNAL_API_URL overrides the default for non-standard deployments.
*/
export function getInternalApiBaseUrl(): string {
const override = process.env.INTERNAL_API_URL;
if (override) {
return override.replace(TRAILING_SLASH, "");
}
return `http://127.0.0.1:${process.env.PORT ?? "3000"}`;
}
26 changes: 17 additions & 9 deletions lib/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {
import { registerMetaTools, registerTools } from "./tools";

async function fetchJson(
baseUrl: string,
internalApiBaseUrl: string,
authHeader: string,
path: string
): Promise<unknown> {
const response = await fetch(`${baseUrl}${path}`, {
const response = await fetch(`${internalApiBaseUrl}${path}`, {
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
Expand All @@ -28,15 +28,19 @@ async function fetchJson(

function registerResources(
server: McpServer,
baseUrl: string,
internalApiBaseUrl: string,
authHeader: string
): void {
server.resource(
"workflows-list",
"keeperhub://workflows",
{ description: "List all workflows for the authenticated organization" },
async (_uri) => {
const data = await fetchJson(baseUrl, authHeader, "/api/workflows");
const data = await fetchJson(
internalApiBaseUrl,
authHeader,
"/api/workflows"
);
return {
contents: [
{
Expand All @@ -59,7 +63,11 @@ function registerResources(
{ description: "Get a specific workflow by ID" },
async (_uri, variables) => {
const id = variables.id;
const data = await fetchJson(baseUrl, authHeader, `/api/workflows/${id}`);
const data = await fetchJson(
internalApiBaseUrl,
authHeader,
`/api/workflows/${id}`
);
return {
contents: [
{
Expand All @@ -74,7 +82,7 @@ function registerResources(
}

export function createMcpServer(
baseUrl: string,
internalApiBaseUrl: string,
authHeader: string,
scope?: string
): McpServer {
Expand All @@ -83,9 +91,9 @@ export function createMcpServer(
version: "1.0.0",
});

registerTools(server, baseUrl, authHeader, scope);
registerMetaTools(server, baseUrl, authHeader, scope);
registerResources(server, baseUrl, authHeader);
registerTools(server, internalApiBaseUrl, authHeader, scope);
registerMetaTools(server, internalApiBaseUrl, authHeader, scope);
registerResources(server, internalApiBaseUrl, authHeader);

return server;
}
Loading
Loading