Skip to content
Open
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
"@sparticuz/chromium": "132.0.0",
"@svelte-put/shortcut": "^3.2.0",
"@sveltejs/adapter-vercel": "^5.7.2",
"@tmcp/adapter-zod-v3": "^0.2.1",
"@tmcp/transport-http": "^0.8.0",
"@types/core-js": "^2.5.8",
"@upstash/redis": "^1.35.1",
"chroma-js": "^2.6.0",
Expand Down Expand Up @@ -115,6 +117,7 @@
"svelte-local-storage-store": "^0.6.4",
"svelte-turnstile": "^0.8.0",
"sveltekit-search-params": "^2.1.2",
"tmcp": "^1.16.1",
"ts-node": "^10.9.2",
"unified": "^11.0.5",
"waait": "^1.0.5",
Expand All @@ -124,5 +127,8 @@
},
"prisma": {
"seed": "node --loader ts-node/esm prisma/seed.ts"
},
"volta": {
"node": "22.20.0"
}
}
101 changes: 101 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { dev } from '$app/environment';
import { UPSPLASH_TOKEN, UPSPLASH_URL } from '$env/static/private';
import { Redis } from '@upstash/redis';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import { transport } from './lib/mcp';

export const cache_status = UPSPLASH_URL && UPSPLASH_TOKEN ? 'ONLINE' : 'OFFLINE';

Expand Down Expand Up @@ -106,6 +107,25 @@ export const safe_form_data: Handle = async function ({ event, resolve }) {
return resolve(event);
};

export const mcp: Handle = async function ({ event, resolve }) {
const mcp_response = await transport.respond(event.request);
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for
// the moment we are not sending back any notifications (logs, or list changed notifications)
// so it's a waste of resources to keep a connection open that will error
// after 5 minutes making the logs dirty. For this reason if we have a response from
// the MCP server and it's a GET request we just return an empty response (it has to be
// 200 or the MCP client will complain)
if (mcp_response && event.request.method === 'GET') {
try {
await mcp_response.body?.cancel();
} catch {
// ignore
}
return new Response('', { status: 200 });
}
return mcp_response ?? resolve(event);
};

// * END HOOKS

// Wraps requests in this sequence of hooks
Expand All @@ -115,7 +135,8 @@ export const handle: Handle = sequence(
auth,
admin,
safe_form_data,
document_policy
document_policy,
mcp
);

export const handleError = Sentry.handleErrorWithSentry();
13 changes: 13 additions & 0 deletions src/lib/mcp/icons/index.ts

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions src/lib/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { McpServer } from 'tmcp';
import { ZodV3JsonSchemaAdapter } from '@tmcp/adapter-zod-v3';
import { icons } from './icons/index.js';
import { HttpTransport } from '@tmcp/transport-http';
import { setup_tools } from './tools';
import { setup_resources } from './resources';

export type SyntaxMCP = typeof server;

const server = new McpServer(
{
description: 'MCP server to access Syntax episodes and transcripts',
name: 'Syntax MCP Server',
websiteUrl: 'https://syntax.fm',
version: '0.1.0',
icons
},
{
adapter: new ZodV3JsonSchemaAdapter(),
capabilities: {
tools: {},
resources: {},
completions: {}
}
}
);

setup_tools(server);
setup_resources(server);

export const transport = new HttpTransport(server, {
cors: true,
path: '/mcp'
});
Loading