diff --git a/src/co-lleague/README.md b/src/co-lleague/README.md new file mode 100644 index 0000000000..f1cdd41197 --- /dev/null +++ b/src/co-lleague/README.md @@ -0,0 +1,62 @@ +# Co-lleague MCP Server + +AGI co-worker for the Model Context Protocol. Query screens, run tabular predictions, execute intents, and check agent status — all from any MCP-compatible client (Claude Desktop, Cursor, Windsurf, VS Code Copilot, OpenCode). + +## Tools + +### `co-lleague_screen_query` +Query what's on the user's screen right now. Returns screen content with AI analysis. + +### `co-lleague_tabular_predict` +Run a tabular prediction on input features. Uses TabICL for in-context learning. + +### `co-lleague_intent_execute` +Execute a natural-language intent. Describe what you want to do and get results. + +### `co-lleague_agent_status` +Get the current agent status — online/offline, active tasks, recent activity. + +## Installation + +### Claude Desktop +Add to your `claude_desktop_config.json`: +```json +{ + "mcpServers": { + "co-lleague": { + "command": "npx", + "args": ["-y", "@aimino/co-lleague-mcp"], + "env": { + "CO_LEAGUE_API_KEY": "your-api-key-here" + } + } + } +} +``` + +### OpenCode / Cline / Continue.dev +```json +{ + "mcpServers": { + "co-lleague": { + "command": "npx", + "args": ["-y", "@aimino/co-lleague-mcp"] + } + } +} +``` + +## Configuration + +| Variable | Required | Default | Description | +|---|---|---|---| +| `CO_LEAGUE_API_KEY` | Yes | — | API key for co-lleague backend | +| `CO_LEAGUE_API_BASE` | No | `https://api.co-lleague.ai` | Backend API base URL | + +## Resources + +- `co-lleague://agents/{agentId}/status` — Live agent status resource + +## License + +MIT diff --git a/src/co-lleague/__tests__/server.test.ts b/src/co-lleague/__tests__/server.test.ts new file mode 100644 index 0000000000..7362f3c8f4 --- /dev/null +++ b/src/co-lleague/__tests__/server.test.ts @@ -0,0 +1,9 @@ +import { describe, it, expect } from "vitest"; + +describe("co-lleague MCP server", () => { + it("should export server configuration", () => { + const pkg = require("../package.json"); + expect(pkg.name).toBe("@modelcontextprotocol/server-co-lleague"); + expect(pkg.bin["mcp-server-co-lleague"]).toBe("dist/index.js"); + }); +}); diff --git a/src/co-lleague/index.ts b/src/co-lleague/index.ts new file mode 100644 index 0000000000..4825a3990d --- /dev/null +++ b/src/co-lleague/index.ts @@ -0,0 +1,197 @@ +#!/usr/bin/env node + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +const API_BASE = process.env.CO_LEAGUE_API_BASE || "https://api.co-lleague.ai"; +const API_KEY = process.env.CO_LEAGUE_API_KEY || ""; +const REQUEST_TIMEOUT_MS = 30_000; + +async function apiPost(path: string, body: unknown) { + const headers: Record = { + "Content-Type": "application/json", + }; + if (API_KEY) headers["x-api-key"] = API_KEY; + + try { + const res = await fetch(`${API_BASE}${path}`, { + method: "POST", + headers, + body: JSON.stringify(body), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + const text = await res.text(); + return { ok: false as const, error: `API ${res.status}: ${text}` }; + } + return { ok: true as const, data: await res.json() }; + } catch (err) { + return { + ok: false as const, + error: `Backend unreachable: ${(err as Error).message}`, + }; + } +} + +async function apiGet(path: string) { + const headers: Record = {}; + if (API_KEY) headers["x-api-key"] = API_KEY; + + try { + const res = await fetch(`${API_BASE}${path}`, { + method: "GET", + headers, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + if (!res.ok) { + const text = await res.text(); + return { ok: false as const, error: `API ${res.status}: ${text}` }; + } + return { ok: true as const, data: await res.json() }; + } catch (err) { + return { + ok: false as const, + error: `Backend unreachable: ${(err as Error).message}`, + }; + } +} + +const server = new McpServer({ + name: "co-lleague", + version: "0.1.0", +}); + +server.tool( + "co-lleague_screen_query", + "Query what's on the user's screen. Returns screen content with AI analysis.", + { + query: z.string().describe("Natural language question about the screen content"), + context: z.string().optional().describe("Additional context for the query"), + }, + async ({ query, context }) => { + const backend = await apiPost("/api/v1/screen/query", { + query, + context: context || "", + }); + + if (backend.ok) { + return { content: [{ type: "text" as const, text: JSON.stringify(backend.data, null, 2) }] }; + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + query, + analysis: "Screen analysis unavailable — backend not reachable", + status: "offline", + }, null, 2), + }, + ], + }; + }, +); + +server.tool( + "co-lleague_tabular_predict", + "Run a tabular prediction on input features.", + { + features: z + .union([z.array(z.number()), z.string()]) + .describe("Input features as an array of numbers or a JSON string"), + target_column: z + .string() + .optional() + .describe("Target column name (for named feature sets)"), + }, + async ({ features, target_column }) => { + const parsed = typeof features === "string" ? JSON.parse(features) : features; + const backend = await apiPost("/api/v1/predict", { + features: parsed, + target_column: target_column || undefined, + }); + + if (backend.ok) { + return { content: [{ type: "text" as const, text: JSON.stringify(backend.data, null, 2) }] }; + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + result: { value: 0.5, confidence: 0.8 }, + note: "Fallback: backend unreachable, used default prediction", + }, null, 2), + }, + ], + }; + }, +); + +server.tool( + "co-lleague_intent_execute", + "Execute a natural-language intent. Returns execution result.", + { + intent: z.string().describe("Natural language description of what to do"), + context: z.string().optional().describe("JSON string with additional context"), + }, + async ({ intent, context }) => { + const parsedCtx = context ? JSON.parse(context) : {}; + const backend = await apiPost("/api/v1/intent/execute", { + intent, + context: parsedCtx, + }); + + if (backend.ok) { + return { content: [{ type: "text" as const, text: JSON.stringify(backend.data, null, 2) }] }; + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + intent, + result: "Execution unavailable — backend not reachable", + status: "offline", + }, null, 2), + }, + ], + }; + }, +); + +server.tool( + "co-lleague_agent_status", + "Get the current agent status — online/offline, active tasks, recent activity.", + { + agent_id: z.string().default("default").describe("Agent identifier"), + }, + async ({ agent_id }) => { + const backend = await apiGet(`/api/v1/agents/${agent_id}/status`); + + if (backend.ok) { + return { content: [{ type: "text" as const, text: JSON.stringify(backend.data, null, 2) }] }; + } + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify({ + agent_id, + status: "offline", + active_tasks: 0, + uptime_hours: 0, + }, null, 2), + }, + ], + }; + }, +); + +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/src/co-lleague/package.json b/src/co-lleague/package.json new file mode 100644 index 0000000000..f74c45c8a0 --- /dev/null +++ b/src/co-lleague/package.json @@ -0,0 +1,46 @@ +{ + "name": "@modelcontextprotocol/server-co-lleague", + "version": "0.1.0", + "description": "MCP server for co-lleague — AGI co-worker: screen analysis, tabular prediction, intent execution", + "license": "MIT", + "mcpName": "io.github.modelcontextprotocol/server-co-lleague", + "author": "Aimino GmbH", + "homepage": "https://co-lleague.ai", + "bugs": "https://github.com/Aimino-Tech/co-lleague/issues", + "repository": { + "type": "git", + "url": "https://github.com/Aimino-Tech/co-lleague.git" + }, + "type": "module", + "bin": { + "mcp-server-co-lleague": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch", + "test": "vitest run --coverage" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^22", + "@vitest/coverage-v8": "^4.1.8", + "shx": "^0.3.4", + "typescript": "^5.8.2", + "vitest": "^4.1.8" + }, + "keywords": [ + "mcp", + "co-lleague", + "ai-agent", + "prediction", + "screen-analysis", + "intent-execution" + ] +} diff --git a/src/co-lleague/tsconfig.json b/src/co-lleague/tsconfig.json new file mode 100644 index 0000000000..db219c5b45 --- /dev/null +++ b/src/co-lleague/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "moduleResolution": "NodeNext", + "module": "NodeNext" + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "**/__tests__/**", + "**/*.test.ts", + "**/*.spec.ts", + "vitest.config.ts" + ] +} diff --git a/src/co-lleague/vitest.config.ts b/src/co-lleague/vitest.config.ts new file mode 100644 index 0000000000..d414ec8f52 --- /dev/null +++ b/src/co-lleague/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['**/__tests__/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['**/*.ts'], + exclude: ['**/__tests__/**', '**/dist/**'], + }, + }, +});