| """ |
| dispatchAI MCP Server - gives the agent tools to interact with the phone farm API. |
| This is a simple MCP server that exposes the dispatchAI API as tools. |
| """ |
| import json |
| import httpx |
| import asyncio |
| from typing import Any |
|
|
| |
| |
|
|
| BASE_URL = "http://127.0.0.1:8081" |
| API_KEY = "da-demo-key-0001" |
|
|
| TOOLS = [ |
| { |
| "name": "chat_completion", |
| "description": "Send a chat completion request to a dispatchAI model running on a phone. Returns the model's response.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "message": {"type": "string", "description": "The user message to send"}, |
| "model": {"type": "string", "description": "Model name (default: SmolLM2-135M)", "default": "dispatchAI/SmolLM2-135M-Instruct-mobile"}, |
| "max_tokens": {"type": "integer", "description": "Max tokens to generate", "default": 100}, |
| }, |
| "required": ["message"], |
| }, |
| }, |
| { |
| "name": "list_models", |
| "description": "List all available dispatchAI models", |
| "inputSchema": {"type": "object", "properties": {}}, |
| }, |
| { |
| "name": "get_phone_status", |
| "description": "Get the status of the phone farm - how many phones are connected and their load", |
| "inputSchema": {"type": "object", "properties": {}}, |
| }, |
| { |
| "name": "get_api_usage", |
| "description": "Get API usage statistics for the current API key", |
| "inputSchema": {"type": "object", "properties": {}}, |
| }, |
| ] |
|
|
|
|
| async def call_tool(name: str, arguments: dict) -> str: |
| """Call a tool and return the result.""" |
| async with httpx.AsyncClient(timeout=120.0) as client: |
| if name == "chat_completion": |
| model = arguments.get("model", "dispatchAI/SmolLM2-135M-Instruct-mobile") |
| message = arguments.get("message", "") |
| max_tokens = arguments.get("max_tokens", 100) |
|
|
| r = await client.post( |
| f"{BASE_URL}/v1/chat/completions", |
| headers={ |
| "Content-Type": "application/json", |
| "Authorization": f"Bearer {API_KEY}", |
| }, |
| json={ |
| "model": model, |
| "messages": [{"role": "user", "content": message}], |
| "max_tokens": max_tokens, |
| }, |
| ) |
| if r.status_code == 200: |
| data = r.json() |
| content = data.get("choices", [{}])[0].get("message", {}).get("content", "") |
| phone = data.get("phone_info", {}) |
| usage = data.get("usage", {}) |
| return json.dumps({ |
| "response": content, |
| "phone": phone.get("serial", "unknown"), |
| "speed_tps": phone.get("generation_tps", 0), |
| "tokens_used": usage.get("total_tokens", 0), |
| }) |
| else: |
| return f"Error: {r.status_code} - {r.text[:200]}" |
|
|
| elif name == "list_models": |
| r = await client.get( |
| f"{BASE_URL}/v1/models", |
| headers={"Authorization": f"Bearer {API_KEY}"}, |
| ) |
| if r.status_code == 200: |
| data = r.json() |
| models = [m["id"] for m in data.get("data", [])] |
| return json.dumps({"models": models}) |
| return f"Error: {r.status_code}" |
|
|
| elif name == "get_phone_status": |
| r = await client.get( |
| f"{BASE_URL}/admin/phones", |
| headers={"Authorization": f"Bearer {API_KEY}"}, |
| ) |
| if r.status_code == 200: |
| return json.dumps(r.json()) |
| return f"Error: {r.status_code}" |
|
|
| elif name == "get_api_usage": |
| r = await client.get( |
| f"{BASE_URL}/v1/usage", |
| headers={"Authorization": f"Bearer {API_KEY}"}, |
| ) |
| if r.status_code == 200: |
| return json.dumps(r.json()) |
| return f"Error: {r.status_code}" |
|
|
| return f"Unknown tool: {name}" |
|
|
|
|
| |
| import sys |
|
|
| def main(): |
| """Main MCP server loop - reads JSON-RPC from stdin, writes to stdout.""" |
| while True: |
| try: |
| line = sys.stdin.readline() |
| if not line: |
| break |
|
|
| req = json.loads(line) |
| method = req.get("method", "") |
| req_id = req.get("id") |
|
|
| if method == "initialize": |
| resp = { |
| "jsonrpc": "2.0", |
| "id": req_id, |
| "result": { |
| "protocolVersion": "2024-11-05", |
| "capabilities": {"tools": {}}, |
| "serverInfo": {"name": "dispatchai-mcp", "version": "1.0.0"}, |
| }, |
| } |
| sys.stdout.write(json.dumps(resp) + "\n") |
| sys.stdout.flush() |
|
|
| elif method == "tools/list": |
| resp = { |
| "jsonrpc": "2.0", |
| "id": req_id, |
| "result": {"tools": TOOLS}, |
| } |
| sys.stdout.write(json.dumps(resp) + "\n") |
| sys.stdout.flush() |
|
|
| elif method == "tools/call": |
| params = req.get("params", {}) |
| tool_name = params.get("name", "") |
| arguments = params.get("arguments", {}) |
|
|
| result = asyncio.run(call_tool(tool_name, arguments)) |
|
|
| resp = { |
| "jsonrpc": "2.0", |
| "id": req_id, |
| "result": { |
| "content": [{"type": "text", "text": result}] |
| }, |
| } |
| sys.stdout.write(json.dumps(resp) + "\n") |
| sys.stdout.flush() |
|
|
| except Exception as e: |
| if req_id: |
| resp = { |
| "jsonrpc": "2.0", |
| "id": req_id, |
| "error": {"code": -32603, "message": str(e)}, |
| } |
| sys.stdout.write(json.dumps(resp) + "\n") |
| sys.stdout.flush() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|