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
3 changes: 1 addition & 2 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ jobs:

- name: Check format and lint
run: |
bun run format
bun run lint
bun run check

# TODO: tests

Expand Down
14 changes: 3 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,12 @@ bun run start
bun run dev
```

### Lint your code
### Format and lint your code

```bash
bun run lint
bun run check
# To fix automatically:
bun run lint:fix
```

### Format your code

```bash
bun run format
# To fix automatically:
bun run format:fix
bun run check:fix
```

### Run individual services
Expand Down
77 changes: 56 additions & 21 deletions apps/bitte-ai/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FastMCP } from 'fastmcp';
import { z } from 'zod';
import { type PluginToolSpec, createToolFromPluginSpec } from './lib/bitte-plugins';
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 @@ -24,6 +24,22 @@ export interface ExecuteAgentParams {
input: string;
}

// Create a log wrapper that also logs to console
function wrapLogger(log: any) {
return new Proxy(log, {
get(target, prop) {
const originalMethod = target[prop];
if (typeof originalMethod === 'function') {
return (...args: any[]) => {
console.log(`[${String(prop)}]`, ...args);
return originalMethod.apply(target, args);
};
}
return originalMethod;
},
});
}

// Create and export the server
export const server = new FastMCP({
name: 'bitte-ai-mcp-proxy',
Expand All @@ -49,9 +65,10 @@ server.addTool({
agentId: z.string().describe('ID of the agent to retrieve'),
}),
execute: async (args, { log }) => {
log.info(`Getting agent with ID: ${args.agentId}`);
const wrappedLog = wrapLogger(log);
wrappedLog.info(`Getting agent with ID: ${args.agentId}`);
const endpoint = `/api/agents/${args.agentId}`;
const data = await callBitteAPI(endpoint, 'GET', undefined, log);
const data = await callBitteAPI(endpoint, 'GET', undefined, wrappedLog);
return JSON.stringify(data);
},
});
Expand All @@ -64,7 +81,8 @@ server.addTool({
input: z.string().describe('Input to the agent'),
}),
execute: async (args, { log, session }) => {
log.info(`Executing agent with ID: ${args.agentId}`);
const wrappedLog = wrapLogger(log);
wrappedLog.info(`Executing agent with ID: ${args.agentId}`);

try {
// First, search for the agent to make sure it exists
Expand All @@ -73,7 +91,7 @@ server.addTool({
query: args.agentId,
threshold: 0.1, // Lower threshold for more exact matching
},
log
wrappedLog
);

// Check if we found a matching agent
Expand All @@ -90,7 +108,7 @@ server.addTool({
};

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

if (typeof data === 'string') {
return {
Expand All @@ -104,7 +122,7 @@ server.addTool({
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error executing agent: ${errorMessage}`);
wrappedLog.error(`Error executing agent: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error executing agent: ${errorMessage}` }],
isError: true,
Expand All @@ -120,11 +138,16 @@ server.addTool({
parameters: z.object({
tool: z.string().describe('The tool to execute'),
params: z.string().describe('The parameters to pass to the tool as a JSON string'),
metadata: z
.object({})
.describe(
'Optional metadata to pass to the tool i.e. {accountId: "123", evmAddress: "0x123"}'
)
.optional(),
}),
execute: async (args, { log }) => {
log.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`);
console.log('execute-tool with args', JSON.stringify(args));
console.log('args', args);
const wrappedLog = wrapLogger(log);
wrappedLog.info(`Executing execute-tool tool with params: ${JSON.stringify(args)}`);

try {
// Use searchTools to find the specified tool
Expand All @@ -133,25 +156,36 @@ server.addTool({
query: args.tool,
threshold: 0.1, // Lower threshold for more exact matching
},
log
wrappedLog
);

// 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>;
execution?: { baseUrl: string; path: string; httpMethod: string };
function?: { name: string; description: string; parameters?: any };
};

if (!tool || typeof tool.execute !== 'function') {
let result: unknown;

// Check if the tool has an execution field
if (tool.execution && tool.function) {
// Create and execute a core tool with HTTP-based execution
const coreTool = createToolFromPluginSpec(tool as PluginToolSpec, args.metadata);
result = await coreTool.execute(JSON.parse(args.params));
} else if (tool.execute && typeof tool.execute === 'function') {
// Use the tool's execute method directly
result = await tool.execute(JSON.parse(args.params));
} else {
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 {
Expand All @@ -164,7 +198,7 @@ server.addTool({
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error executing tool: ${errorMessage}`);
wrappedLog.error(`Error executing tool: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error executing tool: ${errorMessage}` }],
isError: true,
Expand All @@ -179,13 +213,14 @@ server.addTool({
description: 'Search for AI agents across Bitte API and other services',
parameters: searchAgentsSchema,
execute: async (args, { log }) => {
log.info(`Searching agents with params: ${JSON.stringify(args)}`);
const wrappedLog = wrapLogger(log);
wrappedLog.info(`Searching agents with params: ${JSON.stringify(args)}`);
try {
const result = await searchAgents(args, log);
const result = await searchAgents(args, wrappedLog);
return JSON.stringify(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error searching agents: ${errorMessage}`);
wrappedLog.error(`Error searching agents: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error searching agents: ${errorMessage}` }],
isError: true,
Expand All @@ -200,13 +235,13 @@ server.addTool({
description: 'Search for tools across Bitte API and other services',
parameters: searchToolsSchema,
execute: async (args, { log }) => {
log.info(`Searching tools with params: ${JSON.stringify(args)}`);
const wrappedLog = wrapLogger(log);
try {
const result = await searchTools(args, log);
const result = await searchTools(args, wrappedLog);
return JSON.stringify(result);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error(`Error searching tools: ${errorMessage}`);
wrappedLog.error(`Error searching tools: ${errorMessage}`);
return {
content: [{ type: 'text', text: `Error searching tools: ${errorMessage}` }],
isError: true,
Expand Down
117 changes: 117 additions & 0 deletions apps/bitte-ai/lib/bitte-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { z } from 'zod';

export interface ExecutionSpec {
baseUrl: string;
path: string;
httpMethod: string;
}

export interface FunctionSpec {
name: string;
description: string;
parameters?: z.ZodObject<any>;
}

export interface PluginToolSpec {
function: FunctionSpec;
execution: ExecutionSpec;
}

export interface BitteMetadata {
[key: string]: any;
}

// Type for tool execution function
export type BitteToolExecutor = (
args: Record<string, unknown>
) => Promise<{ data?: unknown; error?: string }>;

// Helper function to extract error messages
export function getErrorMsg(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}

export const createExecutor = (
tool: PluginToolSpec,
metadata?: BitteMetadata
): BitteToolExecutor => {
return async (args) => {
try {
const { baseUrl, path, httpMethod } = tool.execution;
const fullBaseUrl = baseUrl.startsWith('http') ? baseUrl : `https://${baseUrl}`;

// Build URL with path parameters
let url = `${fullBaseUrl}${path}`;
const remainingArgs = { ...args };

url = url.replace(/\{(\w+)\}/g, (_, key) => {
if (remainingArgs[key] === undefined) {
throw new Error(`Missing required path parameter: ${key}`);
}
const value = remainingArgs[key];
delete remainingArgs[key];
return encodeURIComponent(String(value));
});

// Setup request
const headers: Record<string, string> = {};
if (metadata) {
headers['mb-metadata'] = JSON.stringify(metadata);
}

const method = httpMethod.toUpperCase();
const fetchOptions: RequestInit = { method, headers };

// Handle query parameters
const queryParams = new URLSearchParams();
for (const [key, value] of Object.entries(remainingArgs)) {
if (value != null) {
queryParams.append(key, String(value));
}
}

const queryString = queryParams.toString();
if (queryString) {
url += (url.includes('?') ? '&' : '?') + queryString;
}

// Handle request body
if (['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(method)) {
headers['Content-Type'] = 'application/json';
fetchOptions.body = JSON.stringify(remainingArgs);
}

// Execute request
const response = await fetch(url, fetchOptions);

if (!response.ok) {
throw new Error(
`HTTP error during plugin tool execution: ${response.status} ${response.statusText}`
);
}
// Parse response based on content type
const contentType = response.headers.get('Content-Type') || '';
const data = await (contentType.includes('application/json')
? response.json()
: contentType.includes('text')
? response.text()
: response.blob());

return { data };
} catch (error) {
return {
error: `Error executing pluginTool ${tool.function.name}. ${getErrorMsg(error)}`,
};
}
};
};

export const createToolFromPluginSpec = (func: PluginToolSpec, metadata?: BitteMetadata) => {
return {
...func.function,
execute: async (args: Record<string, unknown>) => createExecutor(func, metadata)(args),
};
};
Loading