svelte-mcp
v0.0.5
Published
A SvelteKit library for building Model Context Protocol (MCP) servers with reactive Svelte widgets for ChatGPT.
Readme
Chat App Kit
A SvelteKit library for building Model Context Protocol (MCP) servers with reactive Svelte widgets for ChatGPT.
Overview
This library provides three integrated parts that work together:
- Vite Plugin - Compiles
.mcp.sveltefiles into standalone widget bundles - OpenAI Context API - Svelte runes-based API for widget state and ChatGPT integration
- MCP Handler - Server-side handler for tools, resources, and widget serving
How It Works
graph TD
A[.mcp.svelte files] --> B[Vite Plugin]
B --> C[Standalone JS bundles<br/>static/widgets/*.js]
B --> D[Auto-generates<br/>src/mcp/widgets.ts]
D --> E[MCP Handler]
C --> G
E --> F[Serves to ChatGPT<br/>as widget:// resources]
F --> G[ChatGPT loads bundle]
G --> H[Widget auto-mounts<br/>& renders]
H --> I[useOpenAI hook<br/>accesses context]1. Vite Plugin
Compiles .mcp.svelte files into self-contained JavaScript bundles with inlined CSS.
What it does:
- Finds all
.mcp.sveltefiles in your project - Compiles Svelte 5 + Tailwind into standalone IIFE bundles
- Inlines all styles (component CSS + Tailwind utilities)
- Minifies and outputs to
static/widgets/ - Auto-generates
src/mcp/widgets.tswith widget definitions that you can customize if necessary
Setup:
// vite.config.ts
import { mcpSveltePlugin } from 'svelte-mcp/vite';
export default defineConfig({
plugins: [mcpSveltePlugin({ outputDir: 'widgets' })]
});2. OpenAI Context API
Reactive Svelte 5 runes API for accessing ChatGPT state inside .mcp.svelte components.
What it provides:
useOpenAI()- Hook to access ChatGPT context- Reactive widget state with automatic persistence
- Theme, display mode, locale, safe area
- Methods:
requestDisplayMode(),openExternal(),callTool(),sendFollowUpMessage()
Usage:
<script lang="ts">
import { useOpenAI } from 'svelte-mcp';
const openai = useOpenAI({ widgetState: { count: 0 } });
// Reactive access to ChatGPT state
$effect(() => console.log(openai.theme)); // 'light' | 'dark'
// State persists automatically
openai.widgetState.count++;
</script>
<button onclick={() => openai.requestDisplayMode('fullscreen')}>
Expand
</button>3. MCP Handler
SvelteKit server hook that implements the Model Context Protocol to serve widgets, tools, resources, and prompts.
What it does:
- Simplifies widget registration as MCP resources (served as
widget://URIs) - Let you define tools with Zod/Valibot/Arktype schemas
- Links tools to widgets via
.widget()method - Handles all MCP protocol requests (
/mcpendpoint)
Setup:
// src/hooks.server.ts
import { handleMCP, tool } from 'svelte-mcp/mcp';
import * as widgets from './mcp/widgets'; // Auto-generated
import { z } from 'zod';
export const handle = handleMCP({
name: 'my-app',
version: '1.0.0',
domain: 'https://my-app.example.com',
widgets, // Automatically registers all compiled widgets
tools: {
greet: tool('Greet a user')
.input(z.object({ name: z.string() }))
.widget(widgets.greetingWidget) // Links tool to widget
.handle(async ({ input }) => `Hello ${input.name}`)
},
resources: {},
prompts: {}
});How The Parts Connect
- Vite Plugin compiles
Greeting.mcp.svelte→widgets/greeting.js - Vite Plugin generates
widgets.tswithexport const greetingWidget = widget('greeting', {...}) - MCP Handler registers
greetingWidgetas resource atwidget://greeting.js - Tool links to widget via
.widget(greetingWidget)adding metadata - ChatGPT calls tool → handler returns widget URI → ChatGPT loads bundle
- Bundle runs, calls
useOpenAI()to access Context API → widget renders
Key Features
- Standalone bundles - No runtime dependencies, all CSS inlined
- Reactive state - Widget state syncs automatically with ChatGPT
- Type-safe - Full TypeScript support with schema validation
- Hot reload - Changes to
.mcp.sveltefiles reload instantly in dev mode - MCP compliant - Follows Model Context Protocol specification
4. Utilities
Caller
The Caller provides a type-safe RPC-like mechanism for widgets to call MCP tools. It replaces traditional RPC patterns by leveraging ChatGPT's callTool API with full TypeScript inference.
Setup:
1. Export your handler type (server-side):
// src/mcp/hook.ts
import { mcpHandler, tool } from 'svelte-mcp/mcp';
import * as widgets from './widgets';
import { z } from 'zod';
const handler = mcpHandler({
name: 'my-app',
version: '1.0.0',
domain: 'https://my-app.com',
widgets,
tools: {
greet: tool('Greet a user')
.input(z.object({ name: z.string() }))
.output(z.object({ message: z.string() }))
.handle(async ({ input }) => ({ message: `Hello ${input.name}` }))
}
});
export const mcpHandle: Handle = handler.handle;
// Export the tools type for client-side use
export type HANDLER = (typeof handler)['server'];2. Create a typed caller (client-side):
// src/mcp/caller.ts
import { createMcpCaller } from 'svelte-mcp/api';
import type { HANDLER } from './hook';
export const mcp = createMcpCaller<HANDLER>();3. Use in widgets with full type inference:
<script lang="ts">
import { mcp } from '$mcp/caller';
import { useOpenAI } from 'svelte-mcp/api';
const openai = useOpenAI({ widgetState: { greeting: '' } });
async function greet(name: string) {
// Fully typed: mcp.greet expects { name: string }
// Returns: { structuredContent: { message: string }, isError?: boolean, ... }
const result = await mcp.greet({ name });
if (!result.isError && result.structuredContent) {
openai.widgetState.greeting = result.structuredContent.message;
}
}
</script>
<button onclick={() => greet('World')}>Greet</button>
<p>{openai.widgetState.greeting}</p>How it works:
- Your widget calls
mcp.greet({ name: 'World' }) - Caller proxies this to
window.openai.callTool('greet', { name: 'World' }) - ChatGPT sends the tool call to your MCP server's
/mcpendpoint - Your tool handler executes and returns structured output
- ChatGPT forwards the response back to your widget
- Caller returns the typed result with
structuredContent
Type inference:
// Server-side tool definition
export const tools = {
greet: tool('Greet a user')
.input(z.object({ name: z.string() }))
.output(z.object({ message: z.string() }))
.handle(async ({ input }) => ({ message: `Hello ${input.name}` }))
};
// Client-side usage - fully typed!
const mcp = createMcpCaller<typeof tools>();
const result = await mcp.greet({ name: 'World' });
// ^? TypedCallToolResult<typeof tools.greet>
// { structuredContent: { message: string }, isError?: boolean, ... }Key benefits:
- Type-safe RPC - Input/output types inferred from tool schemas
- No HTTP requests - Uses ChatGPT's tool calling infrastructure
- Structured output - Automatically parses
.output()schema intostructuredContent - Error handling - Returns
isErrorflag and error details - Proxy-based - No manual tool registration, works via Proxy
