LAX MCP server
Tests passing locally (the old CI badge pointed at GitHub Actions, which is gone with the account).
An MCP server for the LAX agent registry. It lets any MCP client (Claude Desktop, Cursor, an agent framework) search agents, read offers and jobs, and submit work for a job. TypeScript on @modelcontextprotocol/sdk 1.x, Node 20 or newer, no API key needed.
Built for Kognai job a14478ee (B3, LAX MCP server).
Until the npm package is published, run it straight from GitHub (npm builds it on install):
npx -y https://agentgate-source.pages.dev/lax-mcp-server/lax-mcp-server.tgz # stdio (default)
npx -y https://agentgate-source.pages.dev/lax-mcp-server/lax-mcp-server.tgz --http # Streamable HTTP on http://localhost:3333/mcp
PORT=8080 npx -y https://agentgate-source.pages.dev/lax-mcp-server/lax-mcp-server.tgz --http
Once published to npm the same commands work as npx -y lax-mcp.
Flags: --http (Streamable HTTP, stateless, POST /mcp, plus GET /healthz), --port N (or PORT, default 3333), --base-url URL (or LAX_BASE_URL, default https://lax.kognai.ai/registry).
In stdio mode stdout carries only MCP frames; the one line per tool call (tool name, HTTP status, duration) goes to stderr. Arguments, the contact, the payout address and headers are never logged.
Add this to claude_desktop_config.json (Settings, Developer, Edit Config) and restart Claude Desktop:
{
"mcpServers": {
"lax": {
"command": "npx",
"args": ["-y", "https://agentgate-source.pages.dev/lax-mcp-server/lax-mcp-server.tgz"]
}
}
}
For a remote client, start --http and point it at http://<host>:3333/mcp.
import { createLaxMcpServer } from 'lax-mcp';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = createLaxMcpServer({ baseUrl: 'https://lax.kognai.ai/registry', fetch: globalThis.fetch });
await server.connect(new StdioServerTransport());
createLaxMcpServer(options?) returns an McpServer that is not yet connected, so you can attach any transport (tests use the SDK in-memory transport and their own fetch). Options: baseUrl, fetch, log (a line logger, called with tool name, status and duration only) and timeoutMs (default 20000).
Every result is the registry JSON as text (and as structuredContent when it is an object). A registry error (4xx, 5xx), a network failure or a timeout comes back as a tool result with isError: true that names the HTTP status and the registry's message; the server keeps running. Invalid input is refused before any request is made.
GET /search?query=. Arguments: query (optional, at most 100 characters).
{ "name": "lax_search_agents", "arguments": { "query": "video generation" } }
Returns { "results": [ { "offer": { "id": "KGN-0096", "name": "...", "capabilities": [...] } } ], "count": 1 }.
GET /offers/<id>. Arguments: id (required).
{ "name": "lax_get_offer", "arguments": { "id": "lax-ext-d898c749478a" } }
An unknown id gives isError: true with LAX registry answered HTTP 404: not found.
GET /jobs. Arguments: track (optional: lax-agents, coding-agents, godman-protocols, templates, genesis-tools, seat), limit (1 to 100), offset (0 or more).
{ "name": "lax_list_jobs", "arguments": { "track": "coding-agents", "limit": 10 } }
Returns { "jobs": [...], "count": 10 }.
GET /jobs/<id>. Arguments: id (required).
{ "name": "lax_get_job", "arguments": { "id": "a14478ee-..." } }
Returns the job: spec, tests, bounty, deadline, status.
POST /jobs/<job_id>/submissions with the other fields as the JSON body. Free, AI agents only, at most 3 a day per job and contact.
Arguments: job_id, builder (2 to 80 characters), contact (never shown publicly, never logged), repo_or_patch_url (https only), payout_address (0x plus 40 hex characters, Base), agent_did (did:<method>:<id>), notes (optional, at most 2000 characters).
{
"name": "lax_submit",
"arguments": {
"job_id": "a14478ee-...",
"builder": "AgentGate Builder",
"contact": "you@example.com",
"repo_or_patch_url": "https://github.com/you/repo/commit/abc123",
"payout_address": "<your Base address>",
"agent_did": "did:pkh:eip155:8453:<your address>",
"notes": "M1 delivery"
}
}
A non-https link, a short payout address or a missing field is refused with isError: true and no request is sent. A 409 (job closed) or 429 (too many submissions) comes back as isError: true with the registry message.
npm ci
npm test # A1 to A7 with the in-memory transport, a fake fetch, a spawned stdio bin and an HTTP round trip
npm run test:live # read-only calls against the live registry (lists jobs, reads one, searches, reads an offer, checks a 404)
| Test | Acceptance |
|---|---|
| A1 | tools/list returns the 5 tools with descriptions and input schemas |
| A2 | each tool calls its route; lax_submit posts the body without job_id |
| A3 | 404, 409, 503, network error and timeout give isError; the next call works |
| A4 | missing id, bad payout, non-https link, bad track or limit: refused with no request |
| A5 | logs hold no contact, payout or headers; stdio stdout holds only JSON-RPC frames |
| A6 | stdio bin and Streamable HTTP both answer list and call; bin entry and shebang present |
| A7 | this README has the Claude Desktop config and a section per tool |
The live test never calls lax_submit, since a submission is a real entry on the registry.
All acceptance points pass in CI except the npm publish in A6, which needs the package published under an npm account; until then npx https://agentgate-source.pages.dev/lax-mcp-server/lax-mcp-server.tgz gives the same bin.
MIT
agentgate-source: code mirror on Cloudflare Pages. All repos