honcho/mcp/worker.ts

1345 lines
47 KiB
TypeScript

import { Honcho } from '@honcho-ai/sdk';
interface HonchoConfig {
apiKey: string;
userName: string;
baseUrl?: string;
workspaceId?: string;
assistantName?: string;
}
interface Message {
role: 'user' | 'assistant';
content: string;
metadata?: Record<string, any>;
}
/**
* JSON-RPC 2.0 request interface
*/
interface JsonRpcRequest {
jsonrpc: '2.0';
method: string;
params?: any;
id?: string | number;
}
/**
* JSON-RPC 2.0 response interface
*/
interface JsonRpcResponse {
jsonrpc: '2.0';
id?: string | number | null;
result?: any;
error?: {
code: number;
message: string;
data?: any;
};
}
// MCP Tool definitions
interface Tool {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, any>;
required?: string[];
};
}
/**
* Helper function to validate required arguments and create error responses
*/
function validateArguments(args: Record<string, any>, required: string[], requestId: string | number | null): Response | null {
for (const param of required) {
if (!args[param]) {
return createErrorResponse(requestId, -32602, `${param} is required`);
}
}
// Special validation for arrays
if (args.messages && !Array.isArray(args.messages)) {
return createErrorResponse(requestId, -32602, 'messages must be an array');
}
if (args.peer_ids && !Array.isArray(args.peer_ids)) {
return createErrorResponse(requestId, -32602, 'peer_ids must be an array');
}
return null;
}
/**
* Helper function to create error responses
*/
function createErrorResponse(id: string | number | null, code: number, message: string): Response {
return new Response(JSON.stringify(createJsonRpcResponse(id, undefined, createJsonRpcError(code, message))), {
status: code === -32602 ? 400 : (code === -32601 ? 404 : 500),
headers: { 'Content-Type': 'application/json' },
});
}
/**
* Helper function to format messages for async iteration
*/
async function formatMessages(messagesPage: any): Promise<any[]> {
const messages = [];
for await (const message of messagesPage) {
messages.push({
id: message.id,
content: message.content,
peer_id: message.peer_id,
session_id: message.session_id,
metadata: message.metadata,
created_at: message.created_at,
});
}
return messages;
}
class HonchoWorker {
private honcho: Honcho;
private config: HonchoConfig;
constructor(config: HonchoConfig) {
this.config = {
baseUrl: 'https://api.honcho.dev',
workspaceId: 'default',
assistantName: 'Assistant',
...config,
};
this.honcho = new Honcho({
apiKey: this.config.apiKey,
baseURL: this.config.baseUrl,
workspaceId: this.config.workspaceId,
});
}
////////////////////////////////////////////////////////////////////////////////
/// ///
/// "Bespoke" tools: easy to use for user-assistant conversation paradigms ///
/// ///
////////////////////////////////////////////////////////////////////////////////
/**
* Start a new conversation with a user. Call this when a user starts a new conversation.
* @returns A session ID for the conversation
*/
async startConversation(): Promise<string> {
// Get/create the assistant peer with observe_me=false
const assistant = this.honcho.peer(this.config.assistantName!, { config: { observe_me: false } });
// Create a new session
const sessionId = crypto.randomUUID();
const session = this.honcho.session(sessionId);
// Add the user and assistant peers to the session
// @ts-expect-error - API accepts null for observe_me despite type definition
await session.addPeers([this.config.userName, [assistant, { observe_me: null, observe_others: false }]]);
return sessionId;
}
/**
* Add a turn to a conversation. Call this after a user has sent a message and the assistant has responded.
* @param sessionId - The ID of the session to add the turn to
* @param messages - A list of messages to add to the session
*/
async addTurn(sessionId: string, messages: Message[]): Promise<void> {
const session = this.honcho.session(sessionId);
const userPeer = this.honcho.peer(this.config.userName);
const assistantPeer = this.honcho.peer(this.config.assistantName!);
const sessionMessages = [];
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
// Validate required fields
if (!message || typeof message !== 'object') {
throw new Error(`Message at index ${i} must be a dictionary`);
}
if (!message.role) {
throw new Error(`Message at index ${i} is missing required field 'role'`);
}
if (!message.content) {
throw new Error(`Message at index ${i} is missing required field 'content'`);
}
const { role, content, metadata } = message;
// Create message with appropriate peer
if (role === 'user') {
if (metadata) {
sessionMessages.push(userPeer.message(content, { metadata }));
} else {
sessionMessages.push(userPeer.message(content));
}
} else if (role === 'assistant') {
if (metadata) {
sessionMessages.push(assistantPeer.message(content, { metadata }));
} else {
sessionMessages.push(assistantPeer.message(content));
}
} else {
throw new Error(`Invalid role '${role}' at message index ${i}. Role must be one of: 'user' or 'assistant'`);
}
}
await session.addMessages(sessionMessages);
}
/**
* Get personalization insights about the user, based on the query and the accumulated knowledge of the user across all conversations.
* @param sessionId - The ID of the session for context
* @param query - The question about the user's preferences, habits, etc.
* @returns A string with the personalization insights
*/
async getPersonalizationInsights(sessionId: string, query: string): Promise<string> {
const userPeer = this.honcho.peer(this.config.userName);
// Get the personalization insights
const personalizationInsights = await userPeer.chat(query, { sessionId });
if (!personalizationInsights) {
return "No personalization insights found.";
}
return personalizationInsights;
}
////////////////////////////////////////////////////////////////////////////////
/// ///
/// General tools for using Honcho ///
/// ///
////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////
/// ///
/// Workspace operations ///
/// ///
//////////////////////////////////////////////////////
/**
* Search for messages across the entire workspace.
* @param query - The search query to use
* @returns A list of message dictionaries matching the search query
*/
async searchWorkspace(query: string): Promise<any[]> {
const messagesPage = await this.honcho.search(query);
return await formatMessages(messagesPage);
}
/**
* Get metadata for the current workspace.
* @returns A dictionary containing the workspace's metadata
*/
async getWorkspaceMetadata(): Promise<Record<string, any>> {
return await this.honcho.getMetadata();
}
/**
* Set metadata for the current workspace.
* @param metadata - A dictionary of metadata to associate with the workspace
*/
async setWorkspaceMetadata(metadata: Record<string, any>): Promise<void> {
await this.honcho.setMetadata(metadata);
}
//////////////////////////////////////////////////////
/// ///
/// Peer operations ///
/// ///
//////////////////////////////////////////////////////
/**
* Create or get a peer with the specified ID and optional configuration.
* @param peerId - Unique identifier for the peer
* @param config - Optional configuration dictionary for the peer
* @returns A dictionary with the peer ID and confirmation of creation
*/
async createPeer(peerId: string, config?: Record<string, any>): Promise<{ peer_id: string; config?: Record<string, any> }> {
const peer = this.honcho.peer(peerId, { config });
return {
peer_id: peer.id,
config,
};
}
/**
* Get metadata for a specific peer.
* @param peerId - The ID of the peer to get metadata for
* @returns A dictionary containing the peer's metadata
*/
async getPeerMetadata(peerId: string): Promise<Record<string, any>> {
const peer = this.honcho.peer(peerId);
return await peer.getMetadata();
}
/**
* Set metadata for a specific peer.
* @param peerId - The ID of the peer to set metadata for
* @param metadata - A dictionary of metadata to associate with the peer
*/
async setPeerMetadata(peerId: string, metadata: Record<string, any>): Promise<void> {
const peer = this.honcho.peer(peerId);
await peer.setMetadata(metadata);
}
/**
* Search for messages sent by a peer.
* @param peerId - The ID of the peer to search messages for
* @param query - The search query to use
* @returns A list of message dictionaries matching the search query
*/
async searchPeerMessages(peerId: string, query: string): Promise<any[]> {
const peer = this.honcho.peer(peerId);
const messagesPage = await peer.search(query);
return await formatMessages(messagesPage);
}
/**
* Query a peer's representation with natural language questions.
* @param peerId - The ID of the peer to query
* @param query - The natural language question to ask
* @param targetPeerId - Optional target peer ID for local representation queries
* @param sessionId - Optional session ID to scope the query to a specific session
* @returns Response string containing the answer to the query, or "None" if no relevant information
*/
async chat(peerId: string, query: string, targetPeerId?: string, sessionId?: string): Promise<string> {
const peer = this.honcho.peer(peerId);
let targetPeer;
if (targetPeerId) {
targetPeer = this.honcho.peer(targetPeerId);
}
const result = await peer.chat(query, { target: targetPeer, sessionId });
return result || "None";
}
/**
* Get all peers in the current workspace.
* @returns A list of peer dictionaries with their IDs
*/
async listPeers(): Promise<{ id: string }[]> {
const peersPage = await this.honcho.getPeers();
const peers = [];
for await (const peer of peersPage) {
peers.push({
id: peer.id,
});
}
return peers;
}
//////////////////////////////////////////////////////
/// ///
/// Session operations ///
/// ///
//////////////////////////////////////////////////////
/**
* Create or get a session with the specified ID and optional configuration.
* @param sessionId - Unique identifier for the session
* @param config - Optional configuration dictionary for the session
* @returns A dictionary with the session ID and confirmation of creation
*/
async createSession(sessionId: string, config?: Record<string, any>): Promise<{ session_id: string; config?: Record<string, any> }> {
const session = this.honcho.session(sessionId, { config });
return {
session_id: session.id,
config,
};
}
/**
* Get metadata for a specific session.
* @param sessionId - The ID of the session to get metadata for
* @returns A dictionary containing the session's metadata
*/
async getSessionMetadata(sessionId: string): Promise<Record<string, any>> {
const session = this.honcho.session(sessionId);
return await session.getMetadata();
}
/**
* Set metadata for a specific session.
* @param sessionId - The ID of the session to set metadata for
* @param metadata - A dictionary of metadata to associate with the session
*/
async setSessionMetadata(sessionId: string, metadata: Record<string, any>): Promise<void> {
const session = this.honcho.session(sessionId);
await session.setMetadata(metadata);
}
/**
* Add peers to a session.
* @param sessionId - The ID of the session to add peers to
* @param peerIds - List of peer IDs to add to the session
*/
async addPeersToSession(sessionId: string, peerIds: string[]): Promise<void> {
const session = this.honcho.session(sessionId);
await session.addPeers(peerIds);
}
/**
* Remove peers from a session.
* @param sessionId - The ID of the session to remove peers from
* @param peerIds - List of peer IDs to remove from the session
*/
async removePeersFromSession(sessionId: string, peerIds: string[]): Promise<void> {
const session = this.honcho.session(sessionId);
await session.removePeers(peerIds);
}
/**
* Get all peer IDs in a session.
* @param sessionId - The ID of the session to get peers from
* @returns A list of peer IDs that are members of the session
*/
async getSessionPeers(sessionId: string): Promise<string[]> {
const session = this.honcho.session(sessionId);
const peers = await session.getPeers();
return peers.map(peer => peer.id);
}
/**
* Add messages to a session.
* @param sessionId - The ID of the session to add messages to
* @param messages - List of message dictionaries
*/
async addMessagesToSession(sessionId: string, messages: { peer_id: string; content: string; metadata?: Record<string, any> }[]): Promise<void> {
const session = this.honcho.session(sessionId);
const sessionMessages = [];
for (const message of messages) {
const peer = this.honcho.peer(message.peer_id);
if (message.metadata) {
sessionMessages.push(peer.message(message.content, { metadata: message.metadata }));
} else {
sessionMessages.push(peer.message(message.content));
}
}
await session.addMessages(sessionMessages);
}
/**
* Get messages from a session with optional filtering.
* @param sessionId - The ID of the session to get messages from
* @param filters - Optional dictionary of filter criteria
* @returns A list of message dictionaries
*/
async getSessionMessages(sessionId: string, filters?: Record<string, any>): Promise<any[]> {
const session = this.honcho.session(sessionId);
const messagesPage = await session.getMessages({ filter: filters });
return await formatMessages(messagesPage);
}
/**
* Get optimized context for a session within a token limit.
* @param sessionId - The ID of the session to get context for
* @param summary - Whether to include summary information
* @param tokens - Maximum number of tokens to include in the context
* @returns A dictionary containing the session context with messages and optional summary
*/
async getSessionContext(sessionId: string, summary: boolean = true, tokens?: number): Promise<any> {
const session = this.honcho.session(sessionId);
const context = await session.getContext({ summary, tokens });
return {
session_id: context.sessionId,
summary: context.summary,
messages: context.messages.map(msg => ({
id: msg.id,
content: msg.content,
peer_id: msg.peer_id,
metadata: msg.metadata,
created_at: msg.created_at,
})),
};
}
/**
* Search for messages in a specific session.
* @param sessionId - The ID of the session to search messages in
* @param query - The search query to use
* @returns A list of message dictionaries matching the search query
*/
async searchSessionMessages(sessionId: string, query: string): Promise<any[]> {
const session = this.honcho.session(sessionId);
const messagesPage = await session.search(query);
return await formatMessages(messagesPage);
}
/**
* Get the current working representation of a peer in a session.
* @param sessionId - The ID of the session
* @param peerId - The ID of the peer to get the working representation of
* @param targetPeerId - Optional target peer ID to get the representation of what peer_id knows about target_peer_id
* @returns A dictionary containing information about the peer
*/
async getWorkingRepresentation(sessionId: string, peerId: string, targetPeerId?: string): Promise<Record<string, any>> {
const session = this.honcho.session(sessionId);
if (targetPeerId) {
return await session.workingRep(peerId, targetPeerId);
} else {
return await session.workingRep(peerId);
}
}
/**
* Get all sessions in the current workspace.
* @returns A list of session dictionaries with their IDs
*/
async listSessions(): Promise<{ id: string }[]> {
const sessionsPage = await this.honcho.getSessions();
const sessions = [];
for await (const session of sessionsPage) {
sessions.push({
id: session.id,
});
}
return sessions;
}
}
/**
* Parse configuration from request headers
* @param request - The incoming request
* @returns Configuration object or null if invalid
*/
function parseConfig(request: Request): HonchoConfig | null {
// Get API key from Authorization header
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
const apiKey = authHeader.substring(7);
if (!apiKey) {
return null;
}
const userName = request.headers.get('X-Honcho-User-Name');
if (!userName) {
return null;
}
// Get configuration from headers with proper defaults
const config: HonchoConfig = {
apiKey,
userName,
baseUrl: request.headers.get('X-Honcho-Base-URL') || 'https://api.honcho.dev',
workspaceId: request.headers.get('X-Honcho-Workspace-ID') || 'default',
assistantName: request.headers.get('X-Honcho-Assistant-Name') || 'Assistant',
};
return config;
}
/**
* Create a JSON-RPC 2.0 response
* @param id - Request ID
* @param result - Response result
* @param error - Error object if any
* @returns JSON-RPC response object
*/
function createJsonRpcResponse(id: string | number | null, result?: any, error?: { code: number; message: string; data?: any }): JsonRpcResponse {
const response: JsonRpcResponse = {
jsonrpc: '2.0',
id,
};
if (error) {
response.error = error;
} else {
response.result = result;
}
return response;
}
/**
* Create a JSON-RPC 2.0 error object
* @param code - Error code
* @param message - Error message
* @param data - Optional error data
* @returns Error object
*/
function createJsonRpcError(code: number, message: string, data?: any): { code: number; message: string; data?: any } {
return { code, message, data };
}
// Define all MCP tools based on the Python server.py functions
const tools: Tool[] = [
// Bespoke tools
{
name: 'start_conversation',
description: 'Start a new conversation with a user. Call this when a user starts a new conversation.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'add_turn',
description: 'Add a turn to a conversation. Call this after a user has sent a message and the assistant has responded.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to add the turn to.',
},
messages: {
type: 'array',
description: 'A list of messages to add to the session.',
items: {
type: 'object',
properties: {
role: {
type: 'string',
enum: ['user', 'assistant'],
description: 'The role of the message author.',
},
content: {
type: 'string',
description: 'The content of the message.',
},
metadata: {
type: 'object',
description: 'Optional metadata about the message.',
},
},
required: ['role', 'content'],
},
},
},
required: ['session_id', 'messages'],
},
},
{
name: 'get_personalization_insights',
description: 'Get personalization insights about the user, based on the query and the accumulated knowledge of the user across all conversations.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session for context.',
},
query: {
type: 'string',
description: 'The question about the user\'s preferences, habits, etc.',
},
},
required: ['session_id', 'query'],
},
},
// Workspace operations
{
name: 'search_workspace',
description: 'Search for messages across the entire workspace.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The search query to use.',
},
},
required: ['query'],
},
},
{
name: 'get_workspace_metadata',
description: 'Get metadata for the current workspace.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'set_workspace_metadata',
description: 'Set metadata for the current workspace.',
inputSchema: {
type: 'object',
properties: {
metadata: {
type: 'object',
description: 'A dictionary of metadata to associate with the workspace.',
},
},
required: ['metadata'],
},
},
// Peer operations
{
name: 'create_peer',
description: 'Create or get a peer with the specified ID and optional configuration.',
inputSchema: {
type: 'object',
properties: {
peer_id: {
type: 'string',
description: 'Unique identifier for the peer.',
},
config: {
type: 'object',
description: 'Optional configuration dictionary for the peer.',
},
},
required: ['peer_id'],
},
},
{
name: 'get_peer_metadata',
description: 'Get metadata for a specific peer.',
inputSchema: {
type: 'object',
properties: {
peer_id: {
type: 'string',
description: 'The ID of the peer to get metadata for.',
},
},
required: ['peer_id'],
},
},
{
name: 'set_peer_metadata',
description: 'Set metadata for a specific peer.',
inputSchema: {
type: 'object',
properties: {
peer_id: {
type: 'string',
description: 'The ID of the peer to set metadata for.',
},
metadata: {
type: 'object',
description: 'A dictionary of metadata to associate with the peer.',
},
},
required: ['peer_id', 'metadata'],
},
},
{
name: 'search_peer_messages',
description: 'Search for messages sent by a peer.',
inputSchema: {
type: 'object',
properties: {
peer_id: {
type: 'string',
description: 'The ID of the peer to search messages for.',
},
query: {
type: 'string',
description: 'The search query to use.',
},
},
required: ['peer_id', 'query'],
},
},
{
name: 'chat',
description: 'Query a peer\'s representation with natural language questions.',
inputSchema: {
type: 'object',
properties: {
peer_id: {
type: 'string',
description: 'The ID of the peer to query.',
},
query: {
type: 'string',
description: 'The natural language question to ask.',
},
target_peer_id: {
type: 'string',
description: 'Optional target peer ID for local representation queries.',
},
session_id: {
type: 'string',
description: 'Optional session ID to scope the query to a specific session.',
},
},
required: ['peer_id', 'query'],
},
},
{
name: 'list_peers',
description: 'Get all peers in the current workspace.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
// Session operations
{
name: 'create_session',
description: 'Create or get a session with the specified ID and optional configuration.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'Unique identifier for the session.',
},
config: {
type: 'object',
description: 'Optional configuration dictionary for the session.',
},
},
required: ['session_id'],
},
},
{
name: 'get_session_metadata',
description: 'Get metadata for a specific session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to get metadata for.',
},
},
required: ['session_id'],
},
},
{
name: 'set_session_metadata',
description: 'Set metadata for a specific session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to set metadata for.',
},
metadata: {
type: 'object',
description: 'A dictionary of metadata to associate with the session.',
},
},
required: ['session_id', 'metadata'],
},
},
{
name: 'add_peers_to_session',
description: 'Add peers to a session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to add peers to.',
},
peer_ids: {
type: 'array',
items: { type: 'string' },
description: 'List of peer IDs to add to the session.',
},
},
required: ['session_id', 'peer_ids'],
},
},
{
name: 'remove_peers_from_session',
description: 'Remove peers from a session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to remove peers from.',
},
peer_ids: {
type: 'array',
items: { type: 'string' },
description: 'List of peer IDs to remove from the session.',
},
},
required: ['session_id', 'peer_ids'],
},
},
{
name: 'get_session_peers',
description: 'Get all peer IDs in a session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to get peers from.',
},
},
required: ['session_id'],
},
},
{
name: 'add_messages_to_session',
description: 'Add messages to a session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to add messages to.',
},
messages: {
type: 'array',
items: {
type: 'object',
properties: {
peer_id: {
type: 'string',
description: 'ID of the peer sending the message',
},
content: {
type: 'string',
description: 'Message content',
},
metadata: {
type: 'object',
description: 'Optional metadata dictionary',
},
},
required: ['peer_id', 'content'],
},
description: 'List of message dictionaries.',
},
},
required: ['session_id', 'messages'],
},
},
{
name: 'get_session_messages',
description: 'Get messages from a session with optional filtering.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to get messages from.',
},
filters: {
type: 'object',
description: 'Optional dictionary of filter criteria.',
},
},
required: ['session_id'],
},
},
{
name: 'get_session_context',
description: 'Get optimized context for a session within a token limit.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to get context for.',
},
summary: {
type: 'boolean',
description: 'Whether to include summary information.',
default: true,
},
tokens: {
type: 'integer',
description: 'Maximum number of tokens to include in the context.',
},
},
required: ['session_id'],
},
},
{
name: 'search_session_messages',
description: 'Search for messages in a specific session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session to search messages in.',
},
query: {
type: 'string',
description: 'The search query to use.',
},
},
required: ['session_id', 'query'],
},
},
{
name: 'get_working_representation',
description: 'Get the current working representation of a peer in a session.',
inputSchema: {
type: 'object',
properties: {
session_id: {
type: 'string',
description: 'The ID of the session.',
},
peer_id: {
type: 'string',
description: 'The ID of the peer to get the working representation of.',
},
target_peer_id: {
type: 'string',
description: 'Optional target peer ID to get the representation of what peer_id knows about target_peer_id.',
},
},
required: ['session_id', 'peer_id'],
},
},
{
name: 'list_sessions',
description: 'Get all sessions in the current workspace.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
];
/**
* Execute a tool with validation and consistent response handling
*/
async function executeToolCall(honcho: HonchoWorker, toolName: string, toolArguments: any, requestId: string | number | null): Promise<Response> {
let result: any;
switch (toolName) {
// Bespoke tools
case 'start_conversation':
result = await honcho.startConversation();
break;
case 'add_turn': {
const validation = validateArguments(toolArguments, ['session_id', 'messages'], requestId);
if (validation) return validation;
await honcho.addTurn(toolArguments.session_id, toolArguments.messages);
result = 'Turn added successfully';
break;
}
case 'get_personalization_insights': {
const validation = validateArguments(toolArguments, ['session_id', 'query'], requestId);
if (validation) return validation;
result = await honcho.getPersonalizationInsights(toolArguments.session_id, toolArguments.query);
break;
}
// Workspace operations
case 'search_workspace': {
const validation = validateArguments(toolArguments, ['query'], requestId);
if (validation) return validation;
result = await honcho.searchWorkspace(toolArguments.query);
break;
}
case 'get_workspace_metadata':
result = await honcho.getWorkspaceMetadata();
break;
case 'set_workspace_metadata': {
const validation = validateArguments(toolArguments, ['metadata'], requestId);
if (validation) return validation;
await honcho.setWorkspaceMetadata(toolArguments.metadata);
result = 'Workspace metadata set successfully';
break;
}
// Peer operations
case 'create_peer': {
const validation = validateArguments(toolArguments, ['peer_id'], requestId);
if (validation) return validation;
result = await honcho.createPeer(toolArguments.peer_id, toolArguments.config);
break;
}
case 'get_peer_metadata': {
const validation = validateArguments(toolArguments, ['peer_id'], requestId);
if (validation) return validation;
result = await honcho.getPeerMetadata(toolArguments.peer_id);
break;
}
case 'set_peer_metadata': {
const validation = validateArguments(toolArguments, ['peer_id', 'metadata'], requestId);
if (validation) return validation;
await honcho.setPeerMetadata(toolArguments.peer_id, toolArguments.metadata);
result = 'Peer metadata set successfully';
break;
}
case 'search_peer_messages': {
const validation = validateArguments(toolArguments, ['peer_id', 'query'], requestId);
if (validation) return validation;
result = await honcho.searchPeerMessages(toolArguments.peer_id, toolArguments.query);
break;
}
case 'chat': {
const validation = validateArguments(toolArguments, ['peer_id', 'query'], requestId);
if (validation) return validation;
result = await honcho.chat(toolArguments.peer_id, toolArguments.query, toolArguments.target_peer_id, toolArguments.session_id);
break;
}
case 'list_peers':
result = await honcho.listPeers();
break;
// Session operations
case 'create_session': {
const validation = validateArguments(toolArguments, ['session_id'], requestId);
if (validation) return validation;
result = await honcho.createSession(toolArguments.session_id, toolArguments.config);
break;
}
case 'get_session_metadata': {
const validation = validateArguments(toolArguments, ['session_id'], requestId);
if (validation) return validation;
result = await honcho.getSessionMetadata(toolArguments.session_id);
break;
}
case 'set_session_metadata': {
const validation = validateArguments(toolArguments, ['session_id', 'metadata'], requestId);
if (validation) return validation;
await honcho.setSessionMetadata(toolArguments.session_id, toolArguments.metadata);
result = 'Session metadata set successfully';
break;
}
case 'add_peers_to_session': {
const validation = validateArguments(toolArguments, ['session_id', 'peer_ids'], requestId);
if (validation) return validation;
await honcho.addPeersToSession(toolArguments.session_id, toolArguments.peer_ids);
result = 'Peers added to session successfully';
break;
}
case 'remove_peers_from_session': {
const validation = validateArguments(toolArguments, ['session_id', 'peer_ids'], requestId);
if (validation) return validation;
await honcho.removePeersFromSession(toolArguments.session_id, toolArguments.peer_ids);
result = 'Peers removed from session successfully';
break;
}
case 'get_session_peers': {
const validation = validateArguments(toolArguments, ['session_id'], requestId);
if (validation) return validation;
result = await honcho.getSessionPeers(toolArguments.session_id);
break;
}
case 'add_messages_to_session': {
const validation = validateArguments(toolArguments, ['session_id', 'messages'], requestId);
if (validation) return validation;
await honcho.addMessagesToSession(toolArguments.session_id, toolArguments.messages);
result = 'Messages added to session successfully';
break;
}
case 'get_session_messages': {
const validation = validateArguments(toolArguments, ['session_id'], requestId);
if (validation) return validation;
result = await honcho.getSessionMessages(toolArguments.session_id, toolArguments.filters);
break;
}
case 'get_session_context': {
const validation = validateArguments(toolArguments, ['session_id'], requestId);
if (validation) return validation;
result = await honcho.getSessionContext(toolArguments.session_id, toolArguments.summary, toolArguments.tokens);
break;
}
case 'search_session_messages': {
const validation = validateArguments(toolArguments, ['session_id', 'query'], requestId);
if (validation) return validation;
result = await honcho.searchSessionMessages(toolArguments.session_id, toolArguments.query);
break;
}
case 'get_working_representation': {
const validation = validateArguments(toolArguments, ['session_id', 'peer_id'], requestId);
if (validation) return validation;
result = await honcho.getWorkingRepresentation(toolArguments.session_id, toolArguments.peer_id, toolArguments.target_peer_id);
break;
}
case 'list_sessions':
result = await honcho.listSessions();
break;
default:
return createErrorResponse(requestId, -32601, `Method not found: ${toolName}`);
}
const responseData = typeof result === 'string' ? result : JSON.stringify(result);
return new Response(JSON.stringify(createJsonRpcResponse(requestId, {
content: [{
type: 'text',
text: responseData,
}],
})), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
/**
* Main Cloudflare Worker export
*/
export default {
async fetch(request: Request): Promise<Response> {
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Honcho-User-Name, X-Honcho-Base-URL, X-Honcho-Workspace-ID, X-Honcho-Assistant-Name',
},
});
}
// Only accept POST requests for JSON-RPC
if (request.method !== 'POST') {
return createErrorResponse(null, -32600, 'Invalid Request');
}
let requestData: JsonRpcRequest;
try {
requestData = await request.json() as JsonRpcRequest;
} catch (error) {
return createErrorResponse(null, -32700, 'Parse error');
}
// Validate JSON-RPC format
if (requestData.jsonrpc !== '2.0') {
return createErrorResponse(requestData.id ?? null, -32600, 'Invalid Request');
}
if (!requestData.method) {
return createErrorResponse(requestData.id ?? null, -32600, 'Invalid Request');
}
// Parse configuration
const config = parseConfig(request);
if (!config && requestData.method !== 'initialize') {
return createErrorResponse(requestData.id ?? null, -32602, 'Missing or invalid API key');
}
const honcho = config ? new HonchoWorker(config) : null;
try {
switch (requestData.method) {
case 'initialize':
return new Response(JSON.stringify(createJsonRpcResponse(requestData.id ?? null, {
protocolVersion: '2024-11-05',
capabilities: {
tools: {}
},
serverInfo: {
name: 'Honcho MCP Server',
version: '1.0.0',
},
})), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
case 'notifications/initialized':
// MCP initialized notification - no response needed
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
},
});
case 'tools/list':
return new Response(JSON.stringify(createJsonRpcResponse(requestData.id ?? null, {
tools: tools,
})), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
case 'tools/call':
if (!honcho) {
return createErrorResponse(requestData.id ?? null, -32602, 'Missing API key');
}
const toolName = requestData.params?.name;
const toolArguments = requestData.params?.arguments || {};
return await executeToolCall(honcho, toolName, toolArguments, requestData.id ?? null);
default:
return createErrorResponse(requestData.id ?? null, -32601, `Method not found: ${requestData.method}`);
}
} catch (error) {
console.error('Worker error:', error);
const errorMessage = error instanceof Error ? error.message : 'Internal server error';
return createErrorResponse(requestData.id ?? null, -32603, errorMessage);
}
},
};