Building modern AI-powered applications requires seamless integration between frontend frameworks and AI agents. Svelte's reactive architecture combined with the Model Context Protocol creates a powerful stack for developing intelligent web applications. This guide shows developers how to build MCP-enabled Svelte apps that connect to AI models, databases, and enterprise systems through MintMCP's managed infrastructure.
Key Takeaways
- MCP integration transforms Svelte apps into AI-powered interfaces that interact with Claude, ChatGPT, and custom models through standardized protocols
- Svelte's reactive stores and component architecture align perfectly with MCP's event-driven tool invocation patterns
- MintMCP's gateway architecture eliminates the need to manage MCP servers locally while providing enterprise security and audit trails
- Virtual MCP servers enable role-based access control for different user groups within the same Svelte application
- WebSocket connections through MintMCP provide real-time AI interactions with sub-second latency
- Authentication flows integrate with existing identity providers through SAML SSO and OAuth 2.0
- Production deployments require proper error handling, retry logic, and connection pooling for reliable AI interactions
What is MCP and Why It Matters for Svelte Applications
The Model Context Protocol standardizes how applications interact with AI models and their tools. For Svelte developers, this means building reactive interfaces that connect to AI capabilities without managing individual API integrations for each model or service.
Traditional AI integration follows a fragmented pattern. Each AI provider requires custom API wrappers, authentication logic, and error handling. When your Svelte app needs to connect to Claude, ChatGPT, and internal models, you maintain three separate integration layers with different patterns and no unified observability.
MCP changes this paradigm by providing a standard protocol for tool discovery and invocation. Your Svelte components interact with MCP servers that expose tools as standardized interfaces. The AI models discover and use these tools dynamically, while your application maintains a single integration point.
Svelte's Architecture Advantages for MCP Integration
Svelte's compilation model and reactive primitives make it particularly well-suited for MCP-enabled applications:
Reactive Stores for State Management
Svelte stores naturally model MCP connection state, tool responses, and conversation history. The subscription pattern aligns with MCP's event-driven architecture, enabling automatic UI updates when AI responses arrive.
Component-Based Tool Interfaces
Each MCP tool maps cleanly to a Svelte component. Form inputs bind to tool parameters, while component props define tool capabilities. This creates intuitive interfaces for AI interactions.
Compiled Performance
Svelte's compilation eliminates the runtime overhead common in virtual DOM frameworks. This matters for AI applications where response streaming and real-time updates demand efficient rendering.
Built-in Transitions and Animations
AI responses often arrive asynchronously and in chunks. Svelte's transition system provides smooth visual feedback during loading states and content updates.
MintMCP's Role in Production Svelte Applications
MintMCP bridges the gap between local MCP development and enterprise deployment requirements. Rather than running MCP servers on individual machines or managing infrastructure, teams deploy connectors once through MintMCP's gateway and access them from Svelte applications via secure endpoints.
The gateway provides:
- Centralized Authentication: Users authenticate once through OAuth or SAML rather than managing tokens in frontend code
- Audit Trails: Every tool invocation logs with user attribution for compliance and security
- Tool Governance: Administrators control which tools different user groups can access through Virtual MCP servers
- Performance Optimization: Connection pooling and caching reduce latency for production workloads
Setting Up Your Svelte Development Environment
Before building MCP-enabled features, configure your Svelte project with the necessary dependencies and structure for AI interactions.
Project Initialization
Create a new SvelteKit project optimized for MCP integration:
npm create svelte@latest mcp-svelte-app
cd mcp-svelte-app
npm install
Choose the following options during setup:
- TypeScript for type-safe MCP tool definitions
- ESLint and Prettier for code quality
- Vitest for testing MCP interactions
Installing MCP Client Libraries
Add the MCP client SDK and supporting libraries:
npm install @modelcontextprotocol/sdk-js
npm install socket.io-client
npm install zod
The MCP SDK provides TypeScript types and client utilities. Socket.IO enables real-time WebSocket connections to MintMCP endpoints. Zod validates tool parameters and responses.
Environment Configuration
Create environment files for development and production:
# .env.development
VITE_MINTMCP_GATEWAY_URL=https://gateway.mintmcp.com
VITE_MINTMCP_CLIENT_ID=dev_client_id
VITE_APP_URL=http://localhost:5173
# .env.production
VITE_MINTMCP_GATEWAY_URL=https://gateway.mintmcp.com
VITE_MINTMCP_CLIENT_ID=prod_client_id
VITE_APP_URL=https://your-app.com
Project Structure for MCP Integration
Organize your Svelte project for maintainable MCP integration:
src/
├── lib/
│ ├── mcp/
│ │ ├── client.ts # MCP client initialization
│ │ ├── stores.ts # Reactive stores for MCP state
│ │ ├── tools.ts # Tool definitions and schemas
│ │ └── auth.ts # Authentication logic
│ ├── components/
│ │ ├── MCPChat.svelte # Chat interface component
│ │ ├── ToolForm.svelte # Dynamic tool forms
│ │ └── ResponseView.svelte # AI response display
│ └── utils/
│ ├── errors.ts # Error handling utilities
│ └── retry.ts # Retry logic for connections
├── routes/
│ ├── +layout.svelte # App layout with MCP provider
│ ├── +page.svelte # Main application page
│ └── api/
│ └── mcp/
│ └─
Building the MCP Client Integration
The MCP client manages connections to MintMCP's gateway and handles tool invocations from your Svelte components.
Creating the MCP Client Class
Build a robust client that handles authentication, connection management, and tool discovery:
// src/lib/mcp/client.ts
import { MCPClient, type Tool, type ToolResult } from '@modelcontextprotocol/sdk-js';
import { io, type Socket } from 'socket.io-client';
import { writable, derived, get } from 'svelte/store';
import type { MCPConfig, ConnectionState } from './types';
export class SvelteMCPClient {
private client: MCPClient | null = null;
private socket: Socket | null = null;
private config: MCPConfig;
// Reactive stores
public connectionState = writable<ConnectionState>('disconnected');
public availableTools = writable<Tool[]>([]);
public activeRequests = writable<Map<string, any>>(new Map());
constructor(config: MCPConfig) {
this.config = config;
}
async connect(virtualServerId: string): Promise<void> {
try {
this.connectionState.set('connecting');
// Authenticate with MintMCP
const authToken = await this.authenticate();
// Establish WebSocket connection
this.socket = io(this.config.gatewayUrl, {
auth: { token: authToken },
query: { virtualServerId }
});
// Initialize MCP client
this.client = new MCPClient({
transport: {
send: (message) => this.socket?.emit('mcp:request', message),
onMessage: (handler) => {
this.socket?.on('mcp:response', handler);
}
}
});
// Set up event handlers
this.setupEventHandlers();
// Discover available tools
await this.discoverTools();
this.connectionState.set('connected');
} catch (error) {
this.connectionState.set('error');
throw new Error(`MCP connection failed: ${error.message}`);
}
}
private async authenticate(): Promise<string> {
const response = await fetch(`${this.config.gatewayUrl}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientId: this.config.clientId,
clientSecret: this.config.clientSecret,
})
});
if (!response.ok) {
throw new Error('Authentication failed');
}
const { token } = await response.json();
return token;
}
private setupEventHandlers(): void {
if (!this.socket) return;
this.socket.on('disconnect', () => {
this.connectionState.set('disconnected');
this.reconnect();
});
this.socket.on('error', (error) => {
console.error('MCP socket error:', error);
this.connectionState.set('error');
});
this.socket.on('tool:update', (tools: Tool[]) => {
this.availableTools.set(tools);
});
}
private async discoverTools(): Promise<void> {
if (!this.client) return;
const tools = await this.client.listTools();
this.availableTools.set(tools);
}
async invokeTool(toolName: string, parameters: any): Promise<ToolResult> {
if (!this.client) {
throw new Error('MCP client not connected');
}
const requestId = crypto.randomUUID();
// Track active request
this.activeRequests.update(requests => {
requests.set(requestId, { toolName, startTime: Date.now() });
return requests;
});
try {
const result = await this.client.invokeTool({
name: toolName,
arguments: parameters
});
return result;
} finally {
// Remove from active requests
this.activeRequests.update(requests => {
requests.delete(requestId);
return requests;
});
}
}
private async reconnect(): Promise<void> {
const maxRetries = 5;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
await this.connect(this.config.virtualServerId);
return;
} catch (error) {
retryCount++;
}
}
this.connectionState.set('error');
}
disconnect(): void {
this.socket?.disconnect();
this.client = null;
this.connectionState.set('disconnected');
}
}
Managing MCP State with Svelte Stores
Create reactive stores that components can subscribe to for MCP state updates:
// src/lib/mcp/stores.ts
import { writable, derived, readable } from 'svelte/store';
import { SvelteMCPClient } from './client';
import type { Conversation, Message } from './types';
// Initialize MCP client
const mcpClient = new SvelteMCPClient({
gatewayUrl: import.meta.env.VITE_MINTMCP_GATEWAY_URL,
clientId: import.meta.env.VITE_MINTMCP_CLIENT_ID,
clientSecret: import.meta.env.VITE_MINTMCP_CLIENT_SECRET
});
// Export client stores
export const connectionState = mcpClient.connectionState;
export const availableTools = mcpClient.availableTools;
export const activeRequests = mcpClient.activeRequests;
// Conversation management
export const conversations = writable<Map<string, Conversation>>(new Map());
export const activeConversationId = writable<string | null>(null);
export const activeConversation = derived(
[conversations, activeConversationId],
([$conversations, $id]) => {
if (!$id) return null;
return $conversations.get($id) || null;
}
);
// Message handling
export async function sendMessage(content: string): Promise<void> {
const conversationId = get(activeConversationId);
if (!conversationId) return;
const message: Message = {
id: crypto.randomUUID(),
role: 'user',
content,
timestamp: Date.now()
};
conversations.update(convs => {
const conv = convs.get(conversationId);
if (conv) {
conv.messages.push(message);
}
return convs;
});
// Process with AI
await processWithAI(message);
}
async function processWithAI(userMessage: Message): Promise<void> {
try {
// Invoke AI processing tool
const result = await mcpClient.invokeTool('process_message', {
message: userMessage.content,
conversationId: get(activeConversationId)
});
// Add AI response to conversation
const aiMessage: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: result.output,
timestamp: Date.now()
};
conversations.update(convs => {
const conv = convs.get(get(activeConversationId)!);
if (conv) {
conv.messages.push(aiMessage);
}
return convs;
});
} catch (error) {
console.error('AI processing failed:', error);
}
}
// Export client methods
export const connectMCP = (virtualServerId: string) => mcpClient.connect(virtualServerId);
export const disconnectMCP = () => mcpClient.disconnect();
export const invokeTool = (name: string, params: any) => mcpClient.invokeTool(name, params);
Creating Reactive Svelte Components for MCP
Build components that leverage Svelte's reactivity to create intuitive MCP interfaces.
Chat Interface Component
Create a chat component that handles message input and displays AI responses:
<!-- src/lib/components/MCPChat.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { fade, slide } from 'svelte/transition';
import {
connectionState,
activeConversation,
sendMessage,
activeRequests
} from '$lib/mcp/stores';
import ResponseView from './ResponseView.svelte';
let messageInput = '';
let chatContainer: HTMLElement;
let isProcessing = false;
$: isConnected = $connectionState === 'connected';
$: hasActiveRequests = $activeRequests.size > 0;
async function handleSendMessage() {
if (!messageInput.trim() || !isConnected || isProcessing) return;
isProcessing = true;
const message = messageInput;
messageInput = '';
try {
await sendMessage(message);
} finally {
isProcessing = false;
scrollToBottom();
}
}
function scrollToBottom() {
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}
onMount(() => {
scrollToBottom();
});
</script>
<div class="chat-container">
<div class="connection-status" class:connected={isConnected}>
{#if isConnected}
<span class="status-indicator"></span>
Connected to MCP
{:else if $connectionState === 'connecting'}
<span class="status-indicator connecting"></span>
Connecting...
{:else}
<span class="status-indicator disconnected"></span>
Disconnected
{/if}
</div>
<div class="messages" bind:this={chatContainer}>
{#if $activeConversation}
{#each $activeConversation.messages as message (message.id)}
<div
class="message {message.role}"
in:slide={{ duration: 300 }}
>
<div class="message-content">
{#if message.role === 'assistant'}
<ResponseView content={message.content} />
{:else}
{message.content}
{/if}
</div>
<div class="message-time">
{new Date(message.timestamp).toLocaleTimeString()}
</div>
</div>
{/each}
{#if hasActiveRequests}
<div class="processing-indicator" in:fade>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
{/if}
{:else}
<div class="empty-state">
Start a conversation to begin
</div>
{/if}
</div>
<form on:submit|preventDefault={handleSendMessage} class="input-form">
<input
type="text"
bind:value={messageInput}
placeholder="Type your message..."
disabled={!isConnected || isProcessing}
class="message-input"
/>
<button
type="submit"
disabled={!isConnected || isProcessing || !messageInput.trim()}
class="send-button"
>
Send
</button>
</form>
</div>
<style>
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.connection-status {
padding: 12px;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #dc3545;
}
.connected .status-indicator {
background: #28a745;
}
.connecting .status-indicator {
background: #ffc107;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.message {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.message.user {
align-items: flex-end;
}
.message.assistant {
align-items: flex-start;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 8px;
background: #f0f0f0;
}
.message.user .message-content {
background: #007bff;
color: white;
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 4px;
}
.input-form {
display: flex;
gap: 8px;
padding: 16px;
border-top: 1px solid #e5e5e5;
}
.message-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.send-button {
padding: 10px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.processing-indicator {
display: flex;
gap: 4px;
padding: 8px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #007bff;
animation: bounce 1.4s infinite;
}
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-10px); }
}
</style>
Dynamic Tool Form Component
Generate forms dynamically based on MCP tool schemas:
<!-- src/lib/components/ToolForm.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { z } from 'zod';
import type { Tool } from '@modelcontextprotocol/sdk-js';
export let tool: Tool;
const dispatch = createEventDispatcher();
let formData: Record<string, any> = {};
let errors: Record<string, string> = {};
// Parse tool schema to Zod schema
function parseSchema(inputSchema: any): z.ZodSchema {
if (inputSchema.type === 'object') {
const shape: Record<string, z.ZodSchema> = {};
for (const [key, value] of Object.entries(inputSchema.properties || {})) {
shape[key] = parseFieldSchema(value);
}
return z.object(shape);
}
return z.any();
}
function parseFieldSchema(schema: any): z.ZodSchema {
switch (schema.type) {
case 'string':
return z.string();
case 'number':
return z.number();
case 'boolean':
return z.boolean();
case 'array':
return z.array(parseFieldSchema(schema.items || {}));
default:
return z.any();
}
}
const schema = parseSchema(tool.inputSchema);
function validateField(field: string, value: any) {
try {
const fieldSchema = schema.shape[field];
fieldSchema.parse(value);
errors[field] = '';
} catch (error) {
if (error instanceof z.ZodError) {
errors[field] = error.errors[0].message;
}
}
}
async function handleSubmit() {
try {
const validated = schema.parse(formData);
dispatch('submit', { tool: tool.name, parameters: validated });
} catch (error) {
if (error instanceof z.ZodError) {
errors = error.flatten().fieldErrors;
}
}
}
function getFieldType(schema: any): string {
if (schema.type === 'string' && schema.format === 'email') return 'email';
if (schema.type === 'string' && schema.format === 'url') return 'url';
if (schema.type === 'number') return 'number';
if (schema.type === 'boolean') return 'checkbox';
return 'text';
}
</script>
<form on:submit|preventDefault={handleSubmit} class="tool-form">
<h3>{tool.name}</h3>
{#if tool.description}
<p class="tool-description">{tool.description}</p>
{/if}
{#each Object.entries(tool.inputSchema?.properties || {}) as [field, schema]}
<div class="form-field">
<label for={field}>
{schema.title || field}
{#if tool.inputSchema?.required?.includes(field)}
<span class="required">*</span>
{/if}
</label>
{#if schema.enum}
<select
id={field}
bind:value={formData[field]}
on:change={() => validateField(field, formData[field])}
>
<option value="">Select...</option>
{#each schema.enum as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else if schema.type === 'boolean'}
<input
type="checkbox"
id={field}
bind:checked={formData[field]}
on:change={() => validateField(field, formData[field])}
/>
{:else}
<input
type={getFieldType(schema)}
id={field}
bind:value={formData[field]}
on:blur={() => validateField(field, formData[field])}
placeholder={schema.description}
/>
{/if}
{#if errors[field]}
<span class="error">{errors[field]}</span>
{/if}
</div>
{/each}
<button type="submit" class="submit-button">
Execute Tool
</button>
</form>
<style>
.tool-form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tool-description {
color: #666;
font-size: 14px;
margin-bottom: 20px;
}
.form-field {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #333;
}
.required {
color: #dc3545;
}
input, select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
input[type="checkbox"] {
width: auto;
}
.error {
color: #dc3545;
font-size: 12px;
margin-top: 4px;
display: block;
}
.submit-button {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.submit-button:hover {
background: #0056b3;
}
</style>
Implementing Authentication and Security
Secure MCP integration requires proper authentication flows and token management.
OAuth 2.0 Integration with MintMCP
Implement OAuth authentication that integrates with MintMCP's identity management:
// src/lib/mcp/auth.ts
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
interface AuthConfig {
clientId: string;
redirectUri: string;
gatewayUrl: string;
}
export class MCPAuthManager {
private config: AuthConfig;
private tokenKey = 'mcp_auth_token';
private refreshTokenKey = 'mcp_refresh_token';
constructor(config: AuthConfig) {
this.config = config;
}
initiateOAuthFlow(): void {
const params = new URLSearchParams({
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
response_type: 'code',
scope: 'mcp:read mcp:write',
state: crypto.randomUUID()
});
// Store state for CSRF protection
if (browser) {
sessionStorage.setItem('oauth_state', params.get('state')!);
}
const authUrl = `${this.config.gatewayUrl}/oauth/authorize?${params}`;
window.location.href = authUrl;
}
async handleCallback(code: string, state: string): Promise<void> {
// Verify state parameter
if (browser) {
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('Invalid state parameter');
}
sessionStorage.removeItem('oauth_state');
}
// Exchange code for tokens
const response = await fetch(`${this.config.gatewayUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'authorization_code',
code,
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
this.storeTokens(tokens);
}
private storeTokens(tokens: any): void {
if (!browser) return;
// Store tokens securely
localStorage.setItem(this.tokenKey, tokens.access_token);
localStorage.setItem(this.refreshTokenKey, tokens.refresh_token);
// Set token expiry
const expiresAt = Date.now() + (tokens.expires_in * 1000);
localStorage.setItem('mcp_token_expires', expiresAt.toString());
}
async getValidToken(): Promise<string | null> {
if (!browser) return null;
const token = localStorage.getItem(this.tokenKey);
const expiresAt = localStorage.getItem('mcp_token_expires');
if (!token || !expiresAt) return null;
// Check if token is expired
if (Date.now() >= parseInt(expiresAt)) {
return await this.refreshToken();
}
return token;
}
private async refreshToken(): Promise<string | null> {
const refreshToken = localStorage.getItem(this.refreshTokenKey);
if (!refreshToken) return null;
const response = await fetch(`${this.config.gatewayUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.config.clientId
})
});
if (!response.ok) {
this.clearTokens();
return null;
}
const tokens = await response.json();
this.storeTokens(tokens);
return tokens.access_token;
}
clearTokens(): void {
if (!browser) return;
localStorage.removeItem(this.tokenKey);
localStorage.removeItem(this.refreshTokenKey);
localStorage.removeItem('mcp_token_expires');
}
isAuthenticated(): boolean {
if (!browser) return false;
const token = localStorage.getItem(this.tokenKey);
const expiresAt = localStorage.getItem('mcp_token_expires');
if (!token || !expiresAt) return false;
return Date.now() < parseInt(expiresAt);
}
}
Protected Route Implementation
Create route guards that ensure users authenticate before accessing MCP features:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { MCPAuthManager } from '$lib/mcp/auth';
import { connectMCP } from '$lib/mcp/stores';
const authManager = new MCPAuthManager({
clientId: import.meta.env.VITE_MINTMCP_CLIENT_ID,
redirectUri: `${import.meta.env.VITE_APP_URL}/auth/callback`,
gatewayUrl: import.meta.env.VITE_MINTMCP_GATEWAY_URL
});
onMount(async () => {
// Check authentication status
if (!authManager.isAuthenticated() && $page.url.pathname !== '/login') {
goto('/login');
return;
}
// Connect to MCP if authenticated
if (authManager.isAuthenticated()) {
const virtualServerId = import.meta.env.VITE_VIRTUAL_SERVER_ID;
await connectMCP(virtualServerId);
}
});
</script>
<div class="app-layout">
<nav class="app-nav">
<a href="/">Dashboard</a>
<a href="/tools">Tools</a>
<a href="/settings">Settings</a>
</nav>
<main class="app-content">
<slot />
</main>
</div>
<style>
.app-layout {
display: flex;
height: 100vh;
}
.app-nav {
width: 200px;
background: #f8f9fa;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.app-nav a {
padding: 10px;
text-decoration: none;
color: #333;
border-radius: 4px;
transition: background 0.2s;
}
.app-nav a:hover {
background: #e9ecef;
}
.app-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
</style>
Connecting to MintMCP's Virtual MCP Servers
Virtual MCP servers bundle multiple MCP connectors into manageable endpoints with role-based access control.
Configuring Virtual Server Connections
Set up connections to different Virtual MCP servers based on user roles:
// src/lib/mcp/virtual-servers.ts
import { connectMCP } from './stores';
interface VirtualServer {
id: string;
name: string;
description: string;
capabilities: string[];
requiredRole?: string;
}
export const virtualServers: VirtualServer[] = [
{
id: 'dev-full-access',
name: 'Development Full Access',
description: 'Complete access to all development tools and databases',
capabilities: ['database', 'elasticsearch', 'gmail', 'linear'],
requiredRole: 'developer'
},
{
id: 'analytics-readonly',
name: 'Analytics Read-Only',
description: 'Read-only access to analytics databases',
capabilities: ['snowflake', 'bigquery'],
requiredRole: 'analyst'
},
{
id: 'support-limited',
name: 'Support Limited Access',
description: 'Access to support tools and customer databases',
capabilities: ['linear', 'notion', 'gmail'],
requiredRole: 'support'
}
];
export async function connectToVirtualServer(
serverId: string,
userRole: string
): Promise<void> {
const server = virtualServers.find(s => s.id === serverId);
if (!server) {
throw new Error(`Virtual server ${serverId} not found`);
}
if (server.requiredRole && userRole !== server.requiredRole) {
throw new Error(`Insufficient permissions for ${server.name}`);
}
await connectMCP(serverId);
}
Dynamic Tool Discovery and Loading
Implement dynamic tool discovery that loads capabilities based on the connected Virtual MCP server:
<!-- src/lib/components/ToolExplorer.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import { availableTools, invokeTool } from '$lib/mcp/stores';
import ToolForm from './ToolForm.svelte';
import type { Tool } from '@modelcontextprotocol/sdk-js';
let selectedTool: Tool | null = null;
let searchQuery = '';
let categoryFilter = 'all';
$: filteredTools = $availableTools.filter(tool => {
const matchesSearch = tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description?.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = categoryFilter === 'all' ||
tool.category === categoryFilter;
return matchesSearch && matchesCategory;
});
$: categories = [...new Set($availableTools.map(t => t.category).filter(Boolean))];
async function handleToolSubmit(event: CustomEvent) {
const { tool, parameters } = event.detail;
try {
const result = await invokeTool(tool, parameters);
console.log('Tool result:', result);
// Handle result display
} catch (error) {
console.error('Tool invocation failed:', error);
}
}
</script>
<div class="tool-explorer">
<div class="tool-sidebar">
<div class="search-bar">
<input
type="text"
bind:value={searchQuery}
placeholder="Search tools..."
class="search-input"
/>
</div>
<div class="category-filter">
<select bind:value={categoryFilter}>
<option value="all">All Categories</option>
{#each categories as category}
<option value={category}>{category}</option>
{/each}
</select>
</div>
<div class="tool-list">
{#each filteredTools as tool (tool.name)}
<button
class="tool-item"
class:selected={selectedTool?.name === tool.name}
on:click={() => selectedTool = tool}
>
<div class="tool-name">{tool.name}</div>
{#if tool.description}
<div class="tool-summary">
{tool.description.slice(0, 50)}...
</div>
{/if}
</button>
{/each}
</div>
</div>
<div class="tool-details">
{#if selectedTool}
<ToolForm
tool={selectedTool}
on:submit={handleToolSubmit}
/>
{:else}
<div class="empty-state">
Select a tool to view details
</div>
{/if}
</div>
</div>
<style>
.tool-explorer {
display: flex;
gap: 20px;
height: 100%;
}
.tool-sidebar {
width: 300px;
display: flex;
flex-direction: column;
gap: 16px;
}
.search-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.category-filter select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tool-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.tool-item {
text-align: left;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.tool-item:hover {
border-color: #007bff;
background: #f8f9fa;
}
.tool-item.selected {
border-color: #007bff;
background: #e7f3ff;
}
.tool-name {
font-weight: 500;
margin-bottom: 4px;
}
.tool-summary {
font-size: 12px;
color: #666;
}
.tool-details {
flex: 1;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
</style>
Handling Real-Time MCP Events
MCP's event-driven architecture enables real-time updates in your Svelte application.
WebSocket Event Management
Implement robust WebSocket handling with automatic reconnection:
// src/lib/mcp/websocket.ts
import { writable, get } from 'svelte/store';
import type { Socket } from 'socket.io-client';
export interface WebSocketState {
connected: boolean;
reconnecting: boolean;
error: string | null;
latency: number;
}
export class MCPWebSocketManager {
private socket: Socket | null = null;
public state = writable<WebSocketState>({
connected: false,
reconnecting: false,
error: null,
latency: 0
});
private pingInterval: NodeJS.Timer | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
constructor(private url: string) {}
connect(options: any): Socket {
this.socket = io(this.url, {
...options,
reconnection: false, // Handle manually for better control
timeout: 10000
});
this.setupEventHandlers();
this.startPingInterval();
return this.socket;
}
private setupEventHandlers(): void {
if (!this.socket) return;
this.socket.on('connect', () => {
this.state.update(s => ({
...s,
connected: true,
reconnecting: false,
error: null
}));
this.reconnectAttempts = 0;
});
this.socket.on('disconnect', (reason) => {
this.state.update(s => ({
...s,
connected: false,
error: `Disconnected: ${reason}`
}));
if (reason === 'io server disconnect') {
// Server initiated disconnect, don't reconnect
return;
}
this.attemptReconnect();
});
this.socket.on('error', (error) => {
this.state.update(s => ({
...s,
error: error.message
}));
});
this.socket.on('pong', (latency: number) => {
this.state.update(s => ({
...s,
latency
}));
});
}
private startPingInterval(): void {
this.pingInterval = setInterval(() => {
if (this.socket?.connected) {
const start = Date.now();
this.socket.emit('ping', () => {
const latency = Date.now() - start;
this.state.update(s => ({ ...s, latency }));
});
}
}, 30000); // Ping every 30 seconds
}
private async attemptReconnect(): Promise<void> {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.state.update(s => ({
...s,
error: 'Max reconnection attempts reached'
}));
return;
}
this.state.update(s => ({ ...s, reconnecting: true }));
this.reconnectAttempts++;
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
if (this.socket && !this.socket.connected) {
this.socket.connect();
}
}
disconnect(): void {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
this.socket?.disconnect();
this.socket = null;
this.state.set({
connected: false,
reconnecting: false,
error: null,
latency: 0
});
}
emit(event: string, data: any): void {
if (!this.socket?.connected) {
throw new Error('WebSocket not connected');
}
this.socket.emit(event, data);
}
on(event: string, handler: Function): void {
this.socket?.on(event, handler);
}
off(event: string, handler?: Function): void {
this.socket?.off(event, handler);
}
}
Real-Time Tool Updates
Handle dynamic tool availability changes:
<!-- src/lib/components/RealtimeToolMonitor.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { availableTools } from '$lib/mcp/stores';
import { fly } from 'svelte/transition';
interface ToolChange {
type: 'added' | 'removed' | 'updated';
tool: string;
timestamp: number;
}
let toolChanges: ToolChange[] = [];
let previousTools = new Set<string>();
$: {
const currentTools = new Set($availableTools.map(t => t.name));
// Detect added tools
for (const tool of currentTools) {
if (!previousTools.has(tool)) {
addChange('added', tool);
}
}
// Detect removed tools
for (const tool of previousTools) {
if (!currentTools.has(tool)) {
addChange('removed', tool);
}
}
previousTools = currentTools;
}
function addChange(type: ToolChange['type'], tool: string) {
const change: ToolChange = {
type,
tool,
timestamp: Date.now()
};
toolChanges = [change, ...toolChanges].slice(0, 10);
// Auto-remove after 5 seconds
setTimeout(() => {
toolChanges = toolChanges.filter(c => c !== change);
}, 5000);
}
</script>
<div class="tool-monitor">
<h4>Tool Activity</h4>
<div class="tool-stats">
<div class="stat">
<span class="stat-value">{$availableTools.length}</span>
<span class="stat-label">Available Tools</span>
</div>
</div>
<div class="change-feed">
{#each toolChanges as change (change.timestamp)}
<div
class="change-item {change.type}"
in:fly={{ x: 50, duration: 300 }}
out:fly={{ x: -50, duration: 300 }}
>
<span class="change-type">
{change.type === 'added' ? '+' : '-'}
</span>
<span class="change-tool">{change.tool}</span>
<span class="change-time">
{new Date(change.timestamp).toLocaleTimeString()}
</span>
</div>
{/each}
</div>
</div>
<style>
.tool-monitor {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h4 {
margin: 0 0 16px 0;
color: #333;
}
.tool-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
}
.change-feed {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 200px;
overflow-y: auto;
}
.change-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 4px;
font-size: 14px;
}
.change-item.added {
background: #d4edda;
color: #155724;
}
.change-item.removed {
background: #f8d7da;
color: #721c24;
}
.change-type {
font-weight: bold;
width: 20px;
text-align: center;
}
.change-tool {
flex: 1;
font-family: monospace;
}
.change-time {
font-size: 11px;
color: #999;
}
</style>
Optimizing Performance and Error Handling
Production MCP applications require robust error handling and performance optimization.
Implementing Retry Logic and Circuit Breakers
Create resilient MCP connections with automatic retry and circuit breaking:
// src/lib/mcp/resilience.ts
export class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold: number = 5,
private timeout: number = 60000,
private resetTimeout: number = 30000
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
if (this.state === 'half-open') {
this.reset();
}
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
setTimeout(() => {
this.state = 'half-open';
}, this.resetTimeout);
}
}
private reset(): void {
this.failures = 0;
this.state = 'closed';
}
}
export class RetryManager {
constructor(
private maxAttempts: number = 3,
private backoffMultiplier: number = 2,
private initialDelay: number = 1000
) {}
async execute<T>(
fn: () => Promise<T>,
isRetriable?: (error: any) => boolean
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt < this.maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (isRetriable && !isRetriable(error)) {
throw error;
}
if (attempt < this.maxAttempts - 1) {
const delay = this.initialDelay * Math.pow(this.backoffMultiplier, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
}
Response Caching and Optimization
Implement intelligent caching for MCP tool responses:
// src/lib/mcp/cache.ts
import { writable, get } from 'svelte/store';
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
}
export class MCPResponseCache {
private cache = writable<Map<string, CacheEntry>>(new Map());
constructor(private defaultTTL: number = 300000) {} // 5 minutes default
private generateKey(tool: string, params: any): string {
return `${tool}:${JSON.stringify(params)}`;
}
async get<T>(
tool: string,
params: any,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
const key = this.generateKey(tool, params);
const entries = get(this.cache);
const entry = entries.get(key);
// Check if cached and not expired
if (entry && Date.now() - entry.timestamp < entry.ttl) {
return entry.data as T;
}
// Fetch fresh data
const data = await fetcher();
// Cache the result
this.cache.update(cache => {
cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttl || this.defaultTTL
});
return cache;
});
return data;
}
invalidate(tool?: string, params?: any): void {
if (!tool) {
// Clear entire cache
this.cache.set(new Map());
return;
}
const key = this.generateKey(tool, params || {});
this.cache.update(cache => {
if (params) {
// Clear specific entry
cache.delete(key);
} else {
// Clear all entries for tool
for (const [k] of cache) {
if (k.startsWith(`${tool}:`)) {
cache.delete(k);
}
}
}
return cache;
});
}
preload(tool: string, params: any, data: any, ttl?: number): void {
const key = this.generateKey(tool, params);
this.cache.update(cache => {
cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttl || this.defaultTTL
});
return cache;
});
}
}
Deploying Your MCP-Enabled Svelte Application
Production deployment requires proper configuration and monitoring setup.
Environment Configuration for Production
Set up production environment variables and build configuration:
// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
define: {
'import.meta.env.VITE_MINTMCP_GATEWAY_URL': JSON.stringify(
process.env.MINTMCP_GATEWAY_URL || 'https://gateway.mintmcp.com'
),
'import.meta.env.VITE_VIRTUAL_SERVER_ID': JSON.stringify(
process.env.VIRTUAL_SERVER_ID
),
},
build: {
rollupOptions: {
output: {
manualChunks: {
'mcp-client': ['@modelcontextprotocol/sdk-js'],
'websocket': ['socket.io-client'],
'validation': ['zod']
}
}
}
},
server: {
proxy: {
'/api/mcp': {
target: process.env.MINTMCP_GATEWAY_URL,
changeOrigin: true,
secure: true
}
}
}
});
Docker Deployment Configuration
Create production-ready Docker configuration:
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]
Monitoring and Observability Setup
Implement comprehensive monitoring for your MCP integration:
// src/lib/mcp/monitoring.ts
export class MCPMonitor {
private metrics = {
toolInvocations: new Map<string, number>(),
errors: new Map<string, number>(),
latencies: new Map<string, number[]>()
};
recordToolInvocation(tool: string, latency: number, success: boolean): void {
// Track invocations
const invocations = this.metrics.toolInvocations.get(tool) || 0;
this.metrics.toolInvocations.set(tool, invocations + 1);
// Track latency
const latencies = this.metrics.latencies.get(tool) || [];
latencies.push(latency);
this.metrics.latencies.set(tool, latencies.slice(-100)); // Keep last 100
// Track errors
if (!success) {
const errors = this.metrics.errors.get(tool) || 0;
this.metrics.errors.set(tool, errors + 1);
}
// Send to monitoring service
this.sendMetrics({
tool,
latency,
success,
timestamp: Date.now()
});
}
private async sendMetrics(data: any): Promise<void> {
// Send to your monitoring service
await fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
getMetricsSummary(): any {
const summary: any = {};
for (const [tool, count] of this.metrics.toolInvocations) {
const latencies = this.metrics.latencies.get(tool) || [];
const errors = this.metrics.errors.get(tool) || 0;
summary[tool] = {
invocations: count,
errors,
errorRate: errors / count,
avgLatency: latencies.reduce((a, b) => a + b, 0) / latencies.length,
p95Latency: this.calculatePercentile(latencies, 0.95)
};
}
return summary;
}
private calculatePercentile(values: number[], percentile: number): number {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const index = Math.ceil(percentile * sorted.length) - 1;
return sorted[index];
}
}
Frequently Asked Questions
How does MCP integration affect Svelte's bundle size?
The MCP SDK and supporting libraries add approximately 120KB to your production bundle after gzip compression. Using Vite's code splitting features, you can lazy-load MCP functionality only when needed, reducing initial bundle impact. The WebSocket client (socket.io) adds another 40KB, but this can be dynamically imported when users access AI features. For optimal performance, implement route-based code splitting to load MCP dependencies only on pages that require AI interactions.
Can I use SvelteKit's server-side rendering with MCP connections?
MCP connections should be established client-side to maintain WebSocket connections and handle authentication properly. Initialize the MCP client in onMount hooks or browser-only code blocks. For SEO and initial page load optimization, render static content server-side and hydrate with MCP functionality client-side. Use SvelteKit's +page.server.ts files to fetch initial data through MintMCP's REST APIs, then establish WebSocket connections after hydration for real-time features.
What's the best way to handle offline scenarios in MCP-enabled Svelte apps?
Implement progressive enhancement with offline fallbacks. Cache tool responses using IndexedDB or localStorage for offline access. Detect connection status using the Navigation API and Svelte stores to disable MCP-dependent features gracefully. Queue tool invocations when offline and process them when connectivity returns. For critical features, provide alternative implementations that work without MCP, such as local data processing or cached responses from previous sessions.
How do I test Svelte components that depend on MCP connections?
Create mock MCP clients for unit testing that simulate tool responses without network calls. Use Vitest with mock stores that provide predetermined tool results. For integration testing, connect to MintMCP's sandbox Virtual MCP servers with test data. Implement fixture-based testing where tool responses are recorded and replayed during tests. Use Playwright or Cypress for end-to-end testing with actual MCP connections to verify the complete user flow.
Can I use Svelte's built-in stores with MintMCP's WebSocket events?
Yes, Svelte stores integrate seamlessly with MintMCP's event system. Create custom stores that subscribe to WebSocket events and automatically update component state. Use derived stores to transform raw MCP responses into UI-friendly formats. Implement writable stores for bidirectional data flow between components and MCP tools. The reactive nature of Svelte stores ensures UI updates happen automatically when MCP events arrive, without manual subscription management.
