Add support for in-memory MCP servers
- Add InMemoryTransport support for local servers running in the same process - Update MCPConnectionManager to pass full connection object in callbacks for better context - Filter out local servers from persistence (they auto-connect on startup) - Add 'In-Memory' badge in UI to distinguish local servers - Fix connection state management to properly update connectionsRef - Auto-connect to available in-memory servers on startup This enables testing MCP functionality without external servers by running servers directly in the browser using the InMemoryTransport from the SDK.
This commit is contained in:
parent
c9feacd763
commit
e53be9ec42
6 changed files with 136 additions and 28 deletions
|
|
@ -222,6 +222,11 @@ export function MCPTab() {
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
{connection.name}
|
{connection.name}
|
||||||
</h4>
|
</h4>
|
||||||
|
{connection.url === 'local' && (
|
||||||
|
<span className="px-1.5 py-0.5 text-xs bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-200 rounded">
|
||||||
|
In-Memory
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-xs mt-1 ${getStatusColor(connection.status)}`}>
|
<p className={`text-xs mt-1 ${getStatusColor(connection.status)}`}>
|
||||||
{connection.status}
|
{connection.status}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import type {
|
||||||
} from '@/types/mcp';
|
} from '@/types/mcp';
|
||||||
import type { Tool } from '@/types/inference';
|
import type { Tool } from '@/types/inference';
|
||||||
import { MCPConnectionManager } from '@/mcp/connection';
|
import { MCPConnectionManager } from '@/mcp/connection';
|
||||||
|
import { availableServers } from '@/mcp/servers';
|
||||||
|
|
||||||
const MCPContext = createContext<MCPContextValue | null>(null);
|
const MCPContext = createContext<MCPContextValue | null>(null);
|
||||||
|
|
||||||
|
|
@ -56,24 +57,18 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
|
|
||||||
// Function to broadcast messages to all callbacks (stable reference)
|
// Function to broadcast messages to all callbacks (stable reference)
|
||||||
const broadcastMessage = useCallback((
|
const broadcastMessage = useCallback((
|
||||||
connectionId: string,
|
connection: MCPConnection,
|
||||||
client: any,
|
client: any,
|
||||||
message: any,
|
message: any,
|
||||||
direction: 'sent' | 'received',
|
direction: 'sent' | 'received',
|
||||||
extra?: any
|
extra?: any
|
||||||
) => {
|
) => {
|
||||||
// console.log(`MCP Message [${direction}] from ${connectionId}:`, message); // Debug log
|
|
||||||
|
|
||||||
// Find connection name using ref to get latest state
|
|
||||||
const connection = connectionsRef.current.find(conn => conn.id === connectionId);
|
|
||||||
const serverName = connection?.name || 'Unknown Server';
|
|
||||||
|
|
||||||
// Store message
|
// Store message
|
||||||
const storedMessage: MCPMonitorMessage = {
|
const storedMessage: MCPMonitorMessage = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
connectionId,
|
connectionId: connection.id,
|
||||||
serverName,
|
serverName: connection?.name || 'Unknown Server',
|
||||||
message,
|
message,
|
||||||
direction,
|
direction,
|
||||||
extra,
|
extra,
|
||||||
|
|
@ -82,7 +77,7 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
|
|
||||||
messageCallbacksRef.current.forEach(callback => {
|
messageCallbacksRef.current.forEach(callback => {
|
||||||
try {
|
try {
|
||||||
callback(connectionId, client, message, direction, extra);
|
callback(connection.id, client, message, direction, extra);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in MCP message callback:', error);
|
console.error('Error in MCP message callback:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -118,11 +113,12 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
|
|
||||||
// Set up callback for connection state updates
|
// Set up callback for connection state updates
|
||||||
manager.setConnectionUpdateCallback(() => {
|
manager.setConnectionUpdateCallback(() => {
|
||||||
setConnections(prev =>
|
setConnections(prev => {
|
||||||
prev.map(conn =>
|
connectionsRef.current = prev.map(conn =>
|
||||||
conn.id === connectionId ? manager.getConnection() : conn
|
conn.id === connectionId ? manager.getConnection() : conn
|
||||||
)
|
)
|
||||||
);
|
return connectionsRef.current
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up message callback
|
// Set up message callback
|
||||||
|
|
@ -149,6 +145,50 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load persisted MCP connections:', error);
|
console.error('Failed to load persisted MCP connections:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After loading persisted connections, add local servers
|
||||||
|
await addLocalServers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const addLocalServers = async () => {
|
||||||
|
// Check if local servers are already added
|
||||||
|
const hasLocalServers = connectionsRef.current.some(conn => conn.url === 'local');
|
||||||
|
if (hasLocalServers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add each available local server
|
||||||
|
for (const serverConfig of availableServers) {
|
||||||
|
try {
|
||||||
|
const connectionId = uuidv4();
|
||||||
|
const manager = new MCPConnectionManager(connectionId, serverConfig);
|
||||||
|
|
||||||
|
// Set up callback for connection state updates
|
||||||
|
manager.setConnectionUpdateCallback(() => {
|
||||||
|
setConnections(prev => {
|
||||||
|
connectionsRef.current = prev.map(conn =>
|
||||||
|
conn.id === connectionId ? manager.getConnection() : conn
|
||||||
|
);
|
||||||
|
return connectionsRef.current;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up message callback
|
||||||
|
manager.setMessageCallback(broadcastMessage);
|
||||||
|
|
||||||
|
// Add to managers map
|
||||||
|
setManagers(prev => new Map(prev).set(connectionId, manager));
|
||||||
|
|
||||||
|
// Add initial connection state
|
||||||
|
setConnections(prev => [...prev, manager.getConnection()]);
|
||||||
|
|
||||||
|
// Auto-connect local servers
|
||||||
|
await manager.connect();
|
||||||
|
console.log(`Connected to local server: ${serverConfig.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to connect to local server ${serverConfig.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadPersistedConnections();
|
loadPersistedConnections();
|
||||||
|
|
@ -157,7 +197,7 @@ export function MCPProvider({ children }: MCPProviderProps) {
|
||||||
const persistConnections = useCallback(() => {
|
const persistConnections = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
// Store both config and connection ID to maintain OAuth token association
|
// Store both config and connection ID to maintain OAuth token association
|
||||||
const connectionData = connections.map(conn => ({
|
const connectionData = connections.filter(conn => conn.url !== 'local').map(conn => ({
|
||||||
id: conn.id,
|
id: conn.id,
|
||||||
config: conn.config
|
config: conn.config
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
||||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import {
|
import {
|
||||||
auth,
|
auth,
|
||||||
|
|
@ -235,7 +236,7 @@ export class MCPConnectionManager {
|
||||||
private healthCheckInterval?: NodeJS.Timeout;
|
private healthCheckInterval?: NodeJS.Timeout;
|
||||||
private oauthProvider?: MCPOAuthProvider;
|
private oauthProvider?: MCPOAuthProvider;
|
||||||
private onConnectionUpdate?: () => void;
|
private onConnectionUpdate?: () => void;
|
||||||
private onMessage?: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
|
private onMessage?: (connection: MCPConnection, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void;
|
||||||
|
|
||||||
constructor(id: string, config: MCPServerConfig) {
|
constructor(id: string, config: MCPServerConfig) {
|
||||||
this.connection = {
|
this.connection = {
|
||||||
|
|
@ -265,7 +266,7 @@ export class MCPConnectionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set callback for message monitoring
|
// Set callback for message monitoring
|
||||||
setMessageCallback(callback: (connectionId: string, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
|
setMessageCallback(callback: (connection: MCPConnection, client: any, message: any, direction: 'sent' | 'received', extra?: any) => void): void {
|
||||||
this.onMessage = callback;
|
this.onMessage = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,14 +287,20 @@ export class MCPConnectionManager {
|
||||||
// Clear any existing connections
|
// Clear any existing connections
|
||||||
await this.disconnect();
|
await this.disconnect();
|
||||||
|
|
||||||
try {
|
// Check if this is a local server
|
||||||
await this.tryStreamableHttp();
|
if (this.connection.url === 'local') {
|
||||||
this.connection.transport = 'streamable-http';
|
await this.tryInMemory();
|
||||||
} catch (error) {
|
this.connection.transport = 'inmemory';
|
||||||
// TODO: jerome - if this is a TypeError: failed to fetch, then there is likely a CORS (or
|
} else {
|
||||||
// Access-Control-Expose-Headers) issue with the server.
|
try {
|
||||||
await this.trySSE();
|
await this.tryStreamableHttp();
|
||||||
this.connection.transport = 'sse';
|
this.connection.transport = 'streamable-http';
|
||||||
|
} catch (error) {
|
||||||
|
// TODO: jerome - if this is a TypeError: failed to fetch, then there is likely a CORS (or
|
||||||
|
// Access-Control-Expose-Headers) issue with the server.
|
||||||
|
await this.trySSE();
|
||||||
|
this.connection.transport = 'sse';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize client capabilities
|
// Initialize client capabilities
|
||||||
|
|
@ -435,6 +442,27 @@ export class MCPConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async tryInMemory(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!this.connection.config.localServer) {
|
||||||
|
throw new Error('Local server function not provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create linked transport pair
|
||||||
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||||
|
|
||||||
|
// Create and connect the server
|
||||||
|
const server = this.connection.config.localServer();
|
||||||
|
await server.connect(serverTransport);
|
||||||
|
|
||||||
|
// Initialize the client with its transport
|
||||||
|
await this.initializeClient(clientTransport);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('InMemory connection failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async initializeClient(transport: Transport): Promise<void> {
|
private async initializeClient(transport: Transport): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const debugTransport = new DebugTransport(transport);
|
const debugTransport = new DebugTransport(transport);
|
||||||
|
|
@ -453,13 +481,13 @@ export class MCPConnectionManager {
|
||||||
// Set up message callbacks to broadcast to UI after client is created
|
// Set up message callbacks to broadcast to UI after client is created
|
||||||
debugTransport.onsendmessage_ = async (message, options) => {
|
debugTransport.onsendmessage_ = async (message, options) => {
|
||||||
if (this.onMessage && this.client) {
|
if (this.onMessage && this.client) {
|
||||||
this.onMessage(this.connection.id, this.client, message, 'sent', { options });
|
this.onMessage(this.connection, this.client, message, 'sent', { options });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debugTransport.onreceivemessage_ = (message, extra) => {
|
debugTransport.onreceivemessage_ = (message, extra) => {
|
||||||
if (this.onMessage && this.client) {
|
if (this.onMessage && this.client) {
|
||||||
this.onMessage(this.connection.id, this.client, message, 'received', extra);
|
this.onMessage(this.connection, this.client, message, 'received', extra);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
10
src/mcp/servers/index.ts
Normal file
10
src/mcp/servers/index.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { createServer as createTestServer } from "./test";
|
||||||
|
import type { MCPServerConfig } from "@/types/mcp";
|
||||||
|
|
||||||
|
export const availableServers: MCPServerConfig[] = [
|
||||||
|
{
|
||||||
|
name: "In-Memory Test Server",
|
||||||
|
url: "local",
|
||||||
|
localServer: createTestServer
|
||||||
|
}
|
||||||
|
];
|
||||||
24
src/mcp/servers/test/index.ts
Normal file
24
src/mcp/servers/test/index.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
||||||
|
export function createServer() {
|
||||||
|
// Create an MCP server
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "demo-server",
|
||||||
|
version: "1.0.0"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add an addition tool
|
||||||
|
server.registerTool("add",
|
||||||
|
{
|
||||||
|
title: "Addition Tool",
|
||||||
|
description: "Add two numbers",
|
||||||
|
inputSchema: { a: z.number(), b: z.number() },
|
||||||
|
},
|
||||||
|
async ({ a, b }) => ({
|
||||||
|
content: [{ type: "text", text: String(a + b) }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import type { TransportSendOptions } from '@modelcontextprotocol/sdk/shared/tran
|
||||||
|
|
||||||
export interface MCPServerConfig {
|
export interface MCPServerConfig {
|
||||||
name: string; // User-provided display name
|
name: string; // User-provided display name
|
||||||
url: string; // Server endpoint URL
|
url: string; // Server endpoint URL (or 'local' for in-memory servers)
|
||||||
authType?: 'none' | 'oauth'; // Default: none
|
authType?: 'none' | 'oauth'; // Default: none
|
||||||
oauthConfig?: {
|
oauthConfig?: {
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
|
|
@ -18,6 +18,7 @@ export interface MCPServerConfig {
|
||||||
redirectUri?: string; // Override default redirect URI
|
redirectUri?: string; // Override default redirect URI
|
||||||
};
|
};
|
||||||
maxReconnectAttempts?: number; // Default: 5
|
maxReconnectAttempts?: number; // Default: 5
|
||||||
|
localServer?: () => any; // Function to create the local server instance (when url === 'local')
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MCPMessage {
|
export interface MCPMessage {
|
||||||
|
|
@ -73,7 +74,7 @@ export interface MCPConnection {
|
||||||
url: string; // Server URL
|
url: string; // Server URL
|
||||||
status: 'connecting' | 'connected' | 'failed' | 'disconnected';
|
status: 'connecting' | 'connected' | 'failed' | 'disconnected';
|
||||||
client?: Client; // MCP SDK client instance
|
client?: Client; // MCP SDK client instance
|
||||||
transport?: 'sse' | 'streamable-http';
|
transport?: 'sse' | 'streamable-http' | 'inmemory';
|
||||||
authType?: 'none' | 'oauth';
|
authType?: 'none' | 'oauth';
|
||||||
|
|
||||||
// Available capabilities
|
// Available capabilities
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue