Handle errors in MCP tools

This commit is contained in:
Carl Atupem
2025-06-27 21:20:59 -04:00
parent 39cb01a35e
commit 53126f1499
9 changed files with 527 additions and 15 deletions
+3
View File
@@ -262,6 +262,9 @@ print(status.json())
Use the computer control API for precise automation:
The core container also exposes an [MCP](https://github.com/rekog-labs/MCP-Nest) endpoint.
Connect your MCP client to `http://localhost:9990/sse` to invoke these tools over SSE.
```javascript
// Take screenshot
POST http://localhost:9990/computer-use
+4 -3
View File
@@ -31,6 +31,7 @@ The foundation of the system - a containerized Linux desktop that provides:
- Consistent environment across different platforms
- Can be customized with additional software
- Accessible via REST API on port 9990
- MCP SSE endpoint available at `/sse`
### 2. AI Agent Service
The brain of the system - orchestrates tasks using an LLM:
@@ -82,7 +83,7 @@ Persistent storage for the agent system:
Claude AI analyzes the task and generates a plan of computer actions
</Step>
<Step title="Action Execution">
Agent sends computer actions to bytebotd daemon via REST API
Agent sends computer actions to bytebotd via REST API or MCP
</Step>
<Step title="Desktop Automation">
bytebotd executes actions (mouse, keyboard, screenshots) on the desktop
@@ -101,7 +102,7 @@ Persistent storage for the agent system:
graph LR
A[Tasks UI] -->|WebSocket| B[Agent Service]
A -->|HTTP Proxy| C[Desktop VNC]
B -->|REST API| D[Desktop API]
B -->|REST/MCP| D[Desktop API]
B -->|SQL| E[PostgreSQL]
B -->|HTTPS| F[Claude AI]
D -->|IPC| G[bytebotd]
@@ -128,7 +129,7 @@ graph LR
### API Security
- **Desktop API**: No authentication by default (localhost only)
- **Desktop API**: No authentication by default (localhost only). Supports REST and MCP.
- **Agent API**: Can be secured with API keys
- **Database**: Password protected, not exposed externally
- **VNC Access**: Optional password protection
+1
View File
@@ -114,6 +114,7 @@ Watch it work in real-time through the embedded VNC viewer in the tasks interfac
| **Tasks UI** | [http://localhost:9992](http://localhost:9992) | Main interface for interacting with the agent |
| **Agent API** | [http://localhost:9991/tasks](http://localhost:9991/tasks) | REST API for programmatic task creation |
| **Computer API** | [http://localhost:9990/computer-use](http://localhost:9990/computer-use) | Low-level desktop control API |
| **MCP SSE** | [http://localhost:9990/sse](http://localhost:9990/sse) | Connect MCP clients for tool access |
## Alternative Deployment Options
+111 -11
View File
@@ -17,6 +17,7 @@
"@nestjs/serve-static": "^5.0.3",
"@nestjs/websockets": "^11.1.2",
"@nut-tree-fork/nut-js": "^4.2.6",
"@rekog/mcp-nest": "^1.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"http-proxy-middleware": "^3.0.5",
@@ -2423,6 +2424,29 @@
"node": ">=8"
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.2.tgz",
"integrity": "sha512-Vx7qOcmoKkR3qhaQ9qf3GxiVKCEu+zfJddHv6x3dY/9P6+uIwJnmuAur5aB+4FDXf41rRrDnOEGkviX5oYZ67w==",
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^6.12.6",
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@napi-rs/nice": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
@@ -3105,6 +3129,24 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@rekog/mcp-nest": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@rekog/mcp-nest/-/mcp-nest-1.6.2.tgz",
"integrity": "sha512-swcu99a/woQ5T1f4E/YJct1w/xzszH83bhzEuZwXh+cYRnQUJKhaJd5Aey/XyOtt/D9lQTKSdnoKdpija8kuWA==",
"license": "MIT",
"dependencies": {
"path-to-regexp": "^8.2.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": ">=1.10.0",
"@nestjs/common": ">=9.0.0",
"@nestjs/core": ">=9.0.0",
"express": ">=4.0.0",
"reflect-metadata": ">=0.1.14",
"zod": ">=3.0.0",
"zod-to-json-schema": ">=3.23.0"
}
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -4299,7 +4341,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -5688,7 +5729,6 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -6421,6 +6461,29 @@
"node": ">=0.8.x"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"license": "MIT",
"peer": true,
"dependencies": {
"eventsource-parser": "^3.0.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz",
"integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -6525,6 +6588,22 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-rate-limit": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/express/node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
@@ -6596,7 +6675,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
@@ -6647,7 +6725,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
@@ -8773,7 +8850,6 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
@@ -9765,7 +9841,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -9918,6 +9993,16 @@
"node": ">=4.0.0"
}
},
"node_modules/pkce-challenge": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@@ -10123,7 +10208,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -10735,7 +10819,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -10748,7 +10831,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -12138,7 +12220,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@@ -12461,7 +12542,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -12748,6 +12828,26 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.25.67",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
"integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.6",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
"license": "ISC",
"peer": true,
"peerDependencies": {
"zod": "^3.24.1"
}
}
}
}
+2 -1
View File
@@ -40,7 +40,8 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
"uiohook-napi": "^1.5.4"
"uiohook-napi": "^1.5.4",
"@rekog/mcp-nest": "^1.6.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
+2
View File
@@ -5,6 +5,7 @@ import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BytebotMcpModule } from './mcp';
@Module({
imports: [
@@ -17,6 +18,7 @@ import { AppService } from './app.service';
}),
ComputerUseModule,
InputTrackingModule,
BytebotMcpModule,
],
controllers: [AppController],
providers: [AppService],
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { McpModule } from '@rekog/mcp-nest';
import { ComputerUseModule } from '../computer-use/computer-use.module';
import { ComputerUseTools } from './computer-use.tools';
@Module({
imports: [
ComputerUseModule,
McpModule.forRoot({
name: 'bytebotd',
version: '0.0.1',
sseEndpoint: '/sse',
}),
],
providers: [ComputerUseTools],
})
export class BytebotMcpModule {}
@@ -0,0 +1,386 @@
import { Injectable } from '@nestjs/common';
import { Tool } from '@rekog/mcp-nest';
import { z } from 'zod';
import { ComputerUseService } from '../computer-use/computer-use.service';
import { MessageContentType } from '@bytebot/shared';
@Injectable()
export class ComputerUseTools {
constructor(private readonly computerUse: ComputerUseService) {}
@Tool({
name: 'computer_move_mouse',
description: 'Move the mouse to specific coordinates.',
parameters: z.object({
coordinates: z.object({ x: z.number(), y: z.number() }),
}),
})
async moveMouse({ coordinates }: { coordinates: { x: number; y: number } }) {
try {
await this.computerUse.action({ action: 'move_mouse', coordinates });
return { content: [{ type: MessageContentType.Text, text: 'mouse moved' }] };
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error moving mouse: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_trace_mouse',
description: 'Trace a path with the mouse.',
parameters: z.object({
path: z.array(z.object({ x: z.number(), y: z.number() })),
holdKeys: z.array(z.string()).optional(),
}),
})
async traceMouse({
path,
holdKeys,
}: {
path: { x: number; y: number }[];
holdKeys?: string[];
}) {
try {
await this.computerUse.action({ action: 'trace_mouse', path, holdKeys });
return {
content: [{ type: MessageContentType.Text, text: 'mouse traced' }],
};
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error tracing mouse: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_click_mouse',
description: 'Perform mouse clicks.',
parameters: z.object({
coordinates: z.object({ x: z.number(), y: z.number() }).optional(),
button: z.enum(['left', 'right', 'middle']),
holdKeys: z.array(z.string()).optional(),
numClicks: z.number(),
}),
})
async clickMouse({
coordinates,
button,
holdKeys,
numClicks,
}: {
coordinates?: { x: number; y: number };
button: 'left' | 'right' | 'middle';
holdKeys?: string[];
numClicks: number;
}) {
try {
await this.computerUse.action({
action: 'click_mouse',
coordinates,
button,
holdKeys,
numClicks,
});
return {
content: [{ type: MessageContentType.Text, text: 'mouse clicked' }],
};
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error clicking mouse: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_press_mouse',
description: 'Press or release a mouse button.',
parameters: z.object({
coordinates: z.object({ x: z.number(), y: z.number() }).optional(),
button: z.enum(['left', 'right', 'middle']),
press: z.enum(['down', 'up']),
}),
})
async pressMouse({
coordinates,
button,
press,
}: {
coordinates?: { x: number; y: number };
button: 'left' | 'right' | 'middle';
press: 'down' | 'up';
}) {
try {
await this.computerUse.action({
action: 'press_mouse',
coordinates,
button,
press,
});
return {
content: [{ type: MessageContentType.Text, text: 'mouse pressed' }],
};
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error pressing mouse: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_drag_mouse',
description: 'Drag the mouse along a path.',
parameters: z.object({
path: z.array(z.object({ x: z.number(), y: z.number() })),
button: z.enum(['left', 'right', 'middle']),
holdKeys: z.array(z.string()).optional(),
}),
})
async dragMouse({
path,
button,
holdKeys,
}: {
path: { x: number; y: number }[];
button: 'left' | 'right' | 'middle';
holdKeys?: string[];
}) {
try {
await this.computerUse.action({
action: 'drag_mouse',
path,
button,
holdKeys,
});
return {
content: [{ type: MessageContentType.Text, text: 'mouse dragged' }],
};
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error dragging mouse: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_scroll',
description: 'Scroll the mouse wheel.',
parameters: z.object({
coordinates: z.object({ x: z.number(), y: z.number() }).optional(),
direction: z.enum(['up', 'down', 'left', 'right']),
numScrolls: z.number(),
holdKeys: z.array(z.string()).optional(),
}),
})
async scroll({
coordinates,
direction,
numScrolls,
holdKeys,
}: {
coordinates?: { x: number; y: number };
direction: 'up' | 'down' | 'left' | 'right';
numScrolls: number;
holdKeys?: string[];
}) {
try {
await this.computerUse.action({
action: 'scroll',
coordinates,
direction,
numScrolls,
holdKeys,
});
return { content: [{ type: MessageContentType.Text, text: 'scrolled' }] };
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error scrolling: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_type_keys',
description: 'Press a sequence of keys.',
parameters: z.object({
keys: z.array(z.string()),
delay: z.number().optional(),
}),
})
async typeKeys({ keys, delay }: { keys: string[]; delay?: number }) {
try {
await this.computerUse.action({ action: 'type_keys', keys, delay });
return { content: [{ type: MessageContentType.Text, text: 'keys typed' }] };
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error typing keys: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_press_keys',
description: 'Hold or release keys.',
parameters: z.object({
keys: z.array(z.string()),
press: z.enum(['down', 'up']),
}),
})
async pressKeys({ keys, press }: { keys: string[]; press: 'down' | 'up' }) {
try {
await this.computerUse.action({ action: 'press_keys', keys, press });
return { content: [{ type: MessageContentType.Text, text: 'keys pressed' }] };
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error pressing keys: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_type_text',
description: 'Type a text string.',
parameters: z.object({
text: z.string(),
delay: z.number().optional(),
}),
})
async typeText({ text, delay }: { text: string; delay?: number }) {
try {
await this.computerUse.action({ action: 'type_text', text, delay });
return { content: [{ type: MessageContentType.Text, text: 'text typed' }] };
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error typing text: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_wait',
description: 'Wait for a period of time.',
parameters: z.object({ duration: z.number() }),
})
async wait({ duration }: { duration: number }) {
try {
await this.computerUse.action({ action: 'wait', duration });
return { content: [{ type: MessageContentType.Text, text: 'waiting done' }] };
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error waiting: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_screenshot',
description: 'Capture a screenshot of the desktop.',
})
async screenshot() {
try {
const shot = (await this.computerUse.action({
action: 'screenshot',
})) as { image: string };
return {
content: [
{
type: MessageContentType.Image,
source: {
type: 'base64',
media_type: 'image/png',
data: shot.image,
},
},
],
};
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error taking screenshot: ${(err as Error).message}`,
},
],
};
}
}
@Tool({
name: 'computer_cursor_position',
description: 'Get the current cursor position.',
})
async cursorPosition() {
try {
const pos = (await this.computerUse.action({
action: 'cursor_position',
})) as { x: number; y: number };
return {
content: [
{
type: MessageContentType.Text,
text: JSON.stringify(pos),
},
],
};
} catch (err) {
return {
content: [
{
type: MessageContentType.Text,
text: `Error getting cursor position: ${(err as Error).message}`,
},
],
};
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from './bytebot-mcp.module';