Назад към всички

mcp-web

// Build frontend apps whose state and actions are exposed to AI agents via MCP. Use when building MCP-Web apps, exposing frontend state to AI, creating AI-controllable web UIs, registering MCP tools, or when the user mentions MCP-Web, MCPWeb, or @mcp-web packages.

$ git log --oneline --stat
stars:8
forks:2
updated:March 1, 2026
SKILL.mdreadonly
SKILL.md Frontmatter
namemcp-web
descriptionBuild frontend apps whose state and actions are exposed to AI agents via MCP. Use when building MCP-Web apps, exposing frontend state to AI, creating AI-controllable web UIs, registering MCP tools, or when the user mentions MCP-Web, MCPWeb, or @mcp-web packages.

MCP-Web

Build web applications that AI agents can understand and control through the Model Context Protocol.

Design Principle: Structure your app around declarative, reactive state that can be easily exposed to AI agents via tools.

Project Structure

your-project/
├── mcp-web.config.{ts,js}  # MCPWeb configuration
├── bridge.ts               # Bridge server entry point
├── agent.ts                # Agent server (optional, for frontend queries)
├── src/
│   ├── schemas.ts          # Zod schemas ONLY (no TypeScript types)
│   ├── types.ts            # TypeScript types derived from schemas via z.infer<>
│   ├── states.ts           # Declarative reactive state management
│   ├── mcp.ts              # MCPWeb instantiation
│   ├── tools.ts            # Tool registration (state tools + action tools)
│   ├── queries/            # Frontend-triggered queries (optional)
│   └── <app files>         # Your application code
└── package.json

File Organization Guidelines

Co-locate when small: If schemas, types, states, or tools would each be under ~100 lines, keep them in single files (schemas.ts, types.ts, states.ts, tools.ts). Co-location makes it easier to reason about all related code together.

Split when large: Only create subdirectories (e.g., schemas/, states/) when individual files exceed ~150-200 lines OR when there are clear domain boundaries.

Separate schemas from types: Always keep Zod schemas and TypeScript types in separate files:

  • schemas.ts - Zod schema definitions only
  • types.ts - TypeScript types derived from schemas using z.infer<>
// types.ts - derive types from schemas, never define types manually
import type { z } from 'zod';
import type { TodoSchema, ProjectSchema } from './schemas';

export type Todo = z.infer<typeof TodoSchema>;
export type Project = z.infer<typeof ProjectSchema>;

The State-Schema-Tool Pattern

1. Define Schemas First (schemas.ts)

Schemas are your contract with the AI. Use .describe() extensively!

import { z } from 'zod';

export const TodoSchema = z.object({
  id: z.string().describe('Unique identifier'),
  title: z.string().min(1).describe('Todo title'),
  completed: z.boolean().describe('Completion status'),
  priority: z.enum(['low', 'medium', 'high']).describe('Priority level'),
}).describe('A single todo item');

2. Create Declarative Reactive State (state.ts)

Use your framework's reactivity for derived values:

// Svelte runes example
let gameState = $state<GameState>(createInitialState());
const validMoves = $derived(getLegalMoves(gameState));

export const state = {
  get gameState() { return gameState; },
  set gameState(value) { gameState = value; },
  get validMoves() { return validMoves; },
};

3. Instantiate MCPWeb (mcp.ts)

import { MCPWeb } from '@mcp-web/core';  // ← Class is MCPWeb (not MCPWebBridge)
import { MCP_WEB_CONFIG } from '../mcp-web.config';

export const mcpWeb = new MCPWeb(MCP_WEB_CONFIG);

4. Register Tools (tools.ts)

Keep all tool registrations in a single tools.ts file when under ~100 lines:

import { mcpWeb } from './mcp';
import { TodosSchema, SettingsSchema } from './schemas';
import { todosAtom, settingsAtom } from './states';
import { getDefaultStore } from 'jotai';

const store = getDefaultStore();

// Expose state - returns [getter, setter, cleanup]
mcpWeb.addStateTools({
  name: 'todos',
  description: 'List of all todo items',
  get: () => store.get(todosAtom),
  set: (value) => store.set(todosAtom, value),
  schema: TodosSchema,
  expand: true,  // For collections
});

// Add actions for complex operations
mcpWeb.addTool({
  name: 'complete_todo',
  description: 'Mark a todo as completed',
  handler: (input) => { /* ... */ },
  inputSchema: CompleteTodoSchema,
});

State Tools vs Action Tools

Use State Tools (addStateTools) for:

  • Simple primitives (strings, numbers, booleans)
  • Fixed-shape objects (always same keys)
  • Configuration and preferences

Use Action Tools (addTool) for:

  • Changing shape of state (add/remove items)
  • Validation or business logic
  • Updating multiple states atomically
  • Operations with side effects

Expanded Tools for Collections

Use expand: true when state includes arrays or records that grow:

mcpWeb.addStateTools({
  name: 'todo_app',
  description: 'Todo application state',
  schema: AppSchema,  // Contains arrays/records
  get: () => store.app,
  set: (value) => { store.app = value; },
  expand: true,  // Generates add/set/delete tools for collections
});

System-Generated Fields

Mark auto-generated fields with system() to hide from AI:

import { system, id } from '@mcp-web/core';

const TodoSchema = z.object({
  id: id(system(z.string().default(() => crypto.randomUUID()))),
  created_at: system(z.number().default(() => Date.now())),
  title: z.string(),  // User-visible
});

Key Design Rules

Schema Design

  • Size: 5-20 properties per setter tool
  • Depth: Keep schemas flat
  • Collections: Use expand: true for arrays/records
  • Descriptions: Use .describe() on objects and properties

State Architecture

  • Fixed-shape → state tools (z.object(), z.enum(), primitives)
  • Dynamic-shape → action tools (z.array(), z.record())
  • Derived values → reactive computation (don't expose to AI)

Optional Values

Use nullable() instead of optional() (JSON doesn't support undefined):

// ❌ Ambiguous
z.object({ description: z.string().optional() })

// ✅ Clear
z.object({ description: z.string().nullable().default(null) })

Connection State

Monitor connection status to handle reconnection or display UI feedback.

Core Library

// Check current state
mcpWeb.connected   // boolean - fully connected and authenticated
mcpWeb.connecting  // boolean - during connection/authentication handshake

// Subscribe to changes (returns unsubscribe function)
const unsubscribe = mcpWeb.onConnectionStateChange(() => {
  console.log('Connection:', mcpWeb.connected);
  console.log('Connecting:', mcpWeb.connecting);
});

// Cleanup on unmount
unsubscribe();

React Integration

const { isConnected, isConnecting } = useMCPWeb();

// Show loading indicator during connection
if (isConnecting) return <Spinner />;

// Show offline warning when disconnected
if (!isConnected) return <OfflineBanner />;

Multi-Session Support

When multiple instances of your app connect to the same bridge (e.g., multiple browser tabs), each gets a unique session ID. By default, Claude sees them as opaque UUIDs. Use sessionName to give sessions human-readable labels:

const mcpWeb = new MCPWeb({
  name: 'Checkers',
  description: 'A checkers game',
  sessionName: 'Game 1',  // Must be unique per auth token
});

Key rules:

  • sessionName is optional — unnamed sessions work as before
  • Names must be unique per auth token — the bridge rejects duplicates with a clean authentication-failed message, and connect() rejects with an Error
  • Auth tokens are shared via localStorage across tabs on the same origin, so Claude sees all sessions through one MCP connection
  • Session IDs are always fresh UUIDs (not persisted in localStorage)

Dynamic name allocation (e.g., for demos with multiple tabs):

// game-names.ts — localStorage-based slot allocator
const STORAGE_KEY = 'game-slots';

export function claimGameName(): { name: string; release: () => void } {
  const slots: (string | null)[] = JSON.parse(
    localStorage.getItem(STORAGE_KEY) || '[]'
  );
  let index = slots.findIndex((s) => s === null);
  if (index === -1) index = slots.length;
  const id = crypto.randomUUID();
  slots[index] = id;
  localStorage.setItem(STORAGE_KEY, JSON.stringify(slots));

  return {
    name: `Game ${index + 1}`,
    release: () => {
      const current: (string | null)[] = JSON.parse(
        localStorage.getItem(STORAGE_KEY) || '[]'
      );
      const i = current.indexOf(id);
      if (i !== -1) {
        current[i] = null;
        localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
      }
    },
  };
}

// mcp-tools.ts
const { name, release } = claimGameName();
export const releaseGameName = release;

export const mcpWeb = new MCPWeb({
  ...MCP_WEB_CONFIG,
  sessionName: name,
});

// App.svelte (or equivalent lifecycle)
onDestroy(() => releaseGameName());

Development Workflow

  1. Define schemas with rich descriptions
  2. Create reactive state using your framework
  3. Register tools (state tools for fixed shapes, actions for operations)
  4. Start bridge: npx tsx bridge.ts (uses new MCPWebBridge(config))
  5. Start app: npm run dev
  6. Configure Claude Desktop with auth token
  7. Test with AI and iterate

Bridge Setup (bridge.ts)

Important: The bridge class is MCPWebBridge (not Bridge):

#!/usr/bin/env tsx
import { MCPWebBridge } from '@mcp-web/bridge';  // ← Class is MCPWebBridge
import { MCP_WEB_CONFIG } from './mcp-web.config';

new MCPWebBridge(MCP_WEB_CONFIG);

Quick Reference

Do:

  • Use declarative reactive state
  • Describe schemas extensively
  • Use state tools for fixed-shape data
  • Use action tools for operations
  • Use expand: true for growing collections
  • Mark system fields with system()
  • Use nullable() instead of optional()
  • Keep schemas in schemas.ts, types in types.ts (separate files)
  • Co-locate related code in single files when under ~100 lines each
  • Use onConnectionStateChange() to react to connection changes

Don't:

  • Expose derived values as state tools
  • Use optional() for fields
  • Create one tool per atomic variable (group related state)
  • Expose entire large state as single tool (split or expand)
  • Mix Zod schemas and TypeScript type definitions in the same file
  • Create overly granular file structures (one atom per file, one tool per file)
  • Import Bridge from @mcp-web/bridge (use MCPWebBridge)

Additional Resources