honcho/mcp/worker.ts

1628 lines
44 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 peers first, before session creation
// This avoids a race condition where peer creation during session.addPeers
// could rollback the session if there's an IntegrityError
const userPeer = await this.honcho.peer(this.config.userName);
const assistantPeer = await this.honcho.peer(this.config.assistantName!, {
config: { observe_me: false },
});
// Create a new session - pass empty config to force API call
const sessionId = crypto.randomUUID();
const session = await this.honcho.session(sessionId, { config: {} });
// Add the user and assistant peers to the session
await session.addPeers([
userPeer,
[assistantPeer, { 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 = await this.honcho.session(sessionId);
const userPeer = await this.honcho.peer(this.config.userName);
const assistantPeer = await 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 = await this.honcho.peer(this.config.userName);
// Get the personalization insights (non-streaming returns string | null)
const personalizationInsights = await userPeer.chat(query, {
session: sessionId,
});
if (!personalizationInsights || typeof personalizationInsights !== 'string') {
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 = await 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 = await 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 = await 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 = await 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 = await this.honcho.peer(peerId);
let targetPeer;
if (targetPeerId) {
targetPeer = await this.honcho.peer(targetPeerId);
}
const result = await peer.chat(query, {
target: targetPeer,
session: sessionId,
});
if (!result || typeof result !== 'string') {
return "None";
}
return result;
}
/**
* 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> }> {
// Always pass config (even if empty) to force the API call to create the session
// Without this, the SDK just creates a local Session object without making an API call
const session = await this.honcho.session(sessionId, { config: 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 = await 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 = await 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 = await 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 = await 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 = await 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 = await this.honcho.session(sessionId);
const sessionMessages = [];
for (const message of messages) {
const peer = await 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 = await 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 = await 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 = await 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 = await 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);
}
},
};