#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createApi, requireConfig, buildQuery } from "./utils/api.js";
import { registerStories } from "./tools/stories.js";
import { registerTags } from "./tools/tags.js";
import { registerComponents } from "./tools/components.js";
import { registerAssets } from "./tools/assets.js";
import { registerReleases } from "./tools/releases.js";
import { registerWorkflows } from "./tools/workflows.js";
import { registerWebhooks } from "./tools/webhooks.js";
import { registerDatasources } from "./tools/datasources.js";
import { registerSpace } from "./tools/space.js";
import { registerMeta } from "./tools/meta.js";
import { registerPing } from "./tools/ping.js";
import { registerAssetFolders } from "./tools/asset_folders.js";
import { registerComponentFolders } from "./tools/component_folders.js";
import { registerAdmin } from "./tools/admin.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import cors from "cors";
const serverVersion = "1.0.0";
const serverName = "storyblok-mcp";
function createServerWithContext(
token: string | undefined,
spaceId: string | undefined,
apiBase: string | undefined
) {
const server = new McpServer({ name: serverName, version: serverVersion });
const api = createApi();
const ctx = {
api: <T>(path: string, opts?: any) =>
api<T>(path, {
...opts,
token: token || opts?.token,
spaceId: spaceId || opts?.spaceId,
apiBase: apiBase || opts?.apiBase,
}),
requireConfig: () => {
if (token && spaceId) return;
requireConfig();
},
buildQuery,
};
registerStories(server, ctx);
registerTags(server, ctx);
registerComponents(server, ctx);
registerAssets(server, ctx);
registerReleases(server, ctx);
registerWorkflows(server, ctx);
registerWebhooks(server, ctx);
registerDatasources(server, ctx);
registerSpace(server, ctx);
registerMeta(server, ctx);
registerPing(server, ctx);
registerAssetFolders(server, ctx);
registerComponentFolders(server, ctx);
registerAdmin(server, ctx);
return server;
}
async function main() {
const isSSE = process.env.MCP_TRANSPORT === "sse" || process.argv.includes("--sse");
if (isSSE) {
const app = express();
app.use(cors());
// Map to keep track of transports per session or similar if needed.
// SSE uses a persistent connection, so we can keep the transport in scope.
app.get("/sse/:spaceId?", async (req, res) => {
// Security Check: Optional API Key for the MCP server itself
const serverKey = process.env.MCP_API_KEY;
if (serverKey) {
const clientKey = req.headers["x-mcp-key"];
if (clientKey !== serverKey) {
console.error("Blocked unauthorized MCP connection attempt.");
res.status(401).send("Unauthorized: Missing or invalid x-mcp-key");
return;
}
}
const auth = req.headers.authorization;
const token = auth?.startsWith("Bearer ") ? auth.slice(7) : auth;
const spaceId = (req.params as any).spaceId || (req.headers["x-space-id"] as string) || (req.query.space_id as string);
const apiBase = (req.headers["x-api-base"] as string) || (req.query.api_base as string);
console.error(`New SSE connection for Space: ${spaceId || "default"}`);
const sessionServer = createServerWithContext(token, spaceId, apiBase);
const transport = new SSEServerTransport("/messages", res);
// Handle message posting for this specific session
app.post("/messages", async (mReq, mRes) => {
await transport.handlePostMessage(mReq, mRes);
});
await sessionServer.connect(transport);
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.error(`Storyblok MCP SSE server running on port ${port}`);
});
} else {
// Standard Stdio Transport
const stdioServer = createServerWithContext(undefined, undefined, undefined);
requireConfig();
const transport = new StdioServerTransport();
await stdioServer.connect(transport);
console.error("Storyblok MCP Stdio server running");
}
}
main().catch((e) => {
console.error("Fatal:", e);
process.exit(1);
});