Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 127 additions & 198 deletions apps/bitte-ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MCP } from '@mcp-sdk/server';
import { FastMCP } from 'fastmcp';
import { z } from 'zod';
import { config } from './config';
import { tools as goatTools } from './tools/goat-sdk';

import { searchAgents, searchAgentsSchema, searchTools, searchToolsSchema } from './lib/search';
import { services } from './tools';
import { callBitteAPI } from './utils/bitte';
// Export configuration
export { config } from './config';

Expand All @@ -25,64 +25,22 @@ export interface ExecuteAgentParams {
}

// Create and export the server
export const server = new MCP({
export const server = new FastMCP({
name: 'bitte-ai-mcp-proxy',
version: '0.0.1',
authenticate: async (req) => {
// TODO: Implement authentication
// These are currently not sent by the client (Cursor for example)
const agentId = req.headers['x-agent-id'];
const accountId = req.headers['x-account-id'];
const bitteApiKey = req.headers['x-bitte-api-key'];

return {
id: `user-${Math.random().toString(36).substring(2, 15)}`,
};
},
});

// Tool to get all agents from Bitte AI API
server.addTool({
name: 'get-all-agents',
description: 'Get a list of AI agents from the Bitte AI registry',
parameters: z.object({
verifiedOnly: z.boolean().optional().default(true),
chainIds: z.string().optional(),
category: z.string().optional(),
limit: z.number().optional().default(50),
offset: z.number().optional().default(0),
}),
execute: async (args, { log }) => {
log.info(`Getting agents with params: ${JSON.stringify(args)}`);

// Build query parameters
const params = new URLSearchParams();
if (args.verifiedOnly !== undefined)
params.append('verifiedOnly', args.verifiedOnly.toString());
if (args.chainIds) params.append('chainIds', args.chainIds);
if (args.category) params.append('category', args.category);
if (args.limit) params.append('limit', args.limit.toString());
if (args.offset) params.append('offset', args.offset.toString());

const url = `${config.bitteRegistryUrl}/api/agents`;

try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return JSON.stringify(data);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error fetching agents: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: 'Error fetching agents',
},
],
isError: true,
};
}
},
});

// Tool to get a specific agent by ID
server.addTool({
name: 'get-agent-by-id',
Expand All @@ -92,76 +50,9 @@ server.addTool({
}),
execute: async (args, { log }) => {
log.info(`Getting agent with ID: ${args.agentId}`);

const url = `${config.bitteRegistryUrl}/api/agents/${args.agentId}`;

try {
const response = await fetch(url);
if (response.status === 404) {
return {
content: [
{
type: 'text',
text: 'Agent not found',
},
],
isError: true,
};
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return JSON.stringify(data);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error fetching agent: ${errorMessage}`);

return {
content: [
{
type: 'text',
text: 'Error fetching agent',
},
],
isError: true,
};
}
},
});

// Tool to get all tools from Bitte AI API
server.addTool({
name: 'get-all-tools',
description: 'Get a list of tools from the Bitte AI registry',
parameters: z.object({
random_string: z.string().optional().describe('Dummy parameter for no-parameter tools'),
}),
execute: async (args, { log }) => {
log.info('Getting tools');

const url = `${config.bitteRegistryUrl}/api/tools`;

try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return JSON.stringify(data);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error fetching tools: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: 'Error fetching tools',
},
],
isError: true,
};
}
const endpoint = `/api/agents/${args.agentId}`;
const data = await callBitteAPI(endpoint, 'GET', undefined, log);
return JSON.stringify(data);
},
});

Expand All @@ -175,112 +66,151 @@ server.addTool({
execute: async (args, { log, session }) => {
log.info(`Executing agent with ID: ${args.agentId}`);

const url = `${config.bitteRuntimeUrl}/chat`;

try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.bitteApiKey}`,
// First, search for the agent to make sure it exists
const searchResult = await searchAgents(
{
query: args.agentId,
threshold: 0.1, // Lower threshold for more exact matching
},
body: JSON.stringify({
id: session?.id,
agentId: args.agentId,
accountId: '', // TODO: find a way to get the account id
messages: [{ role: 'user', content: args.input }],
}),
});
log
);

// Check if we found a matching agent
if (searchResult.bitteResults.length === 0) {
throw new Error(`Agent with ID '${args.agentId}' not found`);
}

// Prepare the body for the API call
const body = {
id: session?.id,
agentId: args.agentId,
accountId: '', // TODO: find a way to get the account id
messages: [{ role: 'user', content: args.input }],
};

// Call the Bitte API to execute the agent
const data = await callBitteAPI('/chat', 'POST', body, log);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
if (typeof data === 'string') {
return {
content: [{ type: 'text', text: data }],
};
}

const data = await response.text();
// Ensure we return a properly typed result
return {
content: [
{
type: 'text',
text: data,
},
],
content: [{ type: 'text', text: JSON.stringify(data) }],
};
} catch (error: unknown) {
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error executing agent: ${errorMessage}`);
return {
content: [
{
type: 'text',
text: `Error executing agent: ${errorMessage}`,
},
],
content: [{ type: 'text', text: `Error executing agent: ${errorMessage}` }],
isError: true,
};
}
},
});

// Add tools from extra-tools

// Tool to get existing tools
// Tool to execute a tool
server.addTool({
name: 'get-existing-tools',
description: 'Get existing tools',
name: 'execute-tool',
description: 'Execute a tool',
parameters: z.object({
service: z.string().describe('The service to get tools from'),
tool: z.string().describe('The tool to execute'),
params: z.string().describe('The parameters to pass to the tool as a JSON string'),
}),
execute: async (args, { log }) => {
log.info(`Executing get-existing-tools tool with params: ${JSON.stringify(args)}`);
log.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`);
console.log('execute-tool with args', JSON.stringify(args));
console.log('args', args);

try {
// Use searchTools to find the specified tool
const searchResult = await searchTools(
{
query: args.tool,
threshold: 0.1, // Lower threshold for more exact matching
},
log
);

switch (args.service) {
case 'goat':
return JSON.stringify(goatTools);
default:
throw new Error(`Unknown service: ${args.service}`);
// Get the first (best) match
const toolMatch = searchResult.combinedResults[0];
if (!toolMatch) {
throw new Error(`Tool '${args.tool}' not found`);
}

const tool = toolMatch.item as {
execute?: (params: Record<string, unknown>) => Promise<unknown>;
};

if (!tool || typeof tool.execute !== 'function') {
throw new Error(`Tool '${args.tool}' found but cannot be executed`);
}

const result = await tool.execute(JSON.parse(args.params));

// Ensure we return a properly typed result
if (typeof result === 'string') {
return {
content: [{ type: 'text', text: result }],
};
}

return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error executing tool: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error executing tool: ${errorMessage}` }],
isError: true,
};
}
},
});

// Tool to get available services
// Tool to search for agents across Bitte API and other services
server.addTool({
name: 'get-available-services',
description: 'Get a list of all available services',
parameters: z.object({}),
name: 'search-agents',
description: 'Search for AI agents across Bitte API and other services',
parameters: searchAgentsSchema,
execute: async (args, { log }) => {
log.info('Executing get-available-services tool');

// Return the list of available services
const services = ['goat'];

return JSON.stringify({
services,
count: services.length,
});
log.info(`Searching agents with params: ${JSON.stringify(args)}`);
try {
const result = await searchAgents(args, log);
return JSON.stringify(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error searching agents: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error searching agents: ${errorMessage}` }],
isError: true,
};
}
},
});

// Tool to execute a tool
// Tool to search for tools across Bitte API and other services
server.addTool({
name: 'execute-tool',
description: 'Execute a tool',
parameters: z.object({
tool: z.string().describe('The tool to execute'),
params: z.object({}).describe('The parameters to pass to the tool'),
}),
name: 'search-tools',
description: 'Search for tools across Bitte API and other services',
parameters: searchToolsSchema,
execute: async (args, { log }) => {
log.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`);

switch (args.tool) {
case 'goat': {
const tool = goatTools.find((t) => t.name === args.tool);
if (!tool) {
throw new Error(`Tool not found: ${args.tool}`);
}
return await tool.execute(args.params);
}
default:
throw new Error(`Unknown tool: ${args.tool}`);
log.info(`Searching tools with params: ${JSON.stringify(args)}`);
try {
const result = await searchTools(args, log);
return JSON.stringify(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error searching tools: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error searching tools: ${errorMessage}` }],
isError: true,
};
}
},
});
Expand All @@ -292,7 +222,6 @@ export async function startServer(port = 3000) {
sse: {
endpoint: '/sse',
port,
host: '0.0.0.0',
},
});
console.log(`Bitte AI MCP Proxy server is running on port ${port}`);
Expand Down
Loading