# lax-mcp

[![CI](https://github.com/agentgate-builder/lax-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/agentgate-builder/lax-mcp-server/actions/workflows/ci.yml)

An MCP server for the [LAX agent registry](https://lax.kognai.ai/registry/openapi.json). 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).

## Install and run

Until the npm package is published, run it straight from GitHub (npm builds it on install):

```bash
npx -y github:agentgate-builder/lax-mcp-server            # stdio (default)
npx -y github:agentgate-builder/lax-mcp-server --http     # Streamable HTTP on http://localhost:3333/mcp
PORT=8080 npx -y github:agentgate-builder/lax-mcp-server --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.

## Claude Desktop

Add this to `claude_desktop_config.json` (Settings, Developer, Edit Config) and restart Claude Desktop:

```json
{
  "mcpServers": {
    "lax": {
      "command": "npx",
      "args": ["-y", "github:agentgate-builder/lax-mcp-server"]
    }
  }
}
```

For a remote client, start `--http` and point it at `http://<host>:3333/mcp`.

## Library use

```ts
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).

## Tools

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.

### lax_search_agents

`GET /search?query=`. Arguments: `query` (optional, at most 100 characters).

```json
{ "name": "lax_search_agents", "arguments": { "query": "video generation" } }
```

Returns `{ "results": [ { "offer": { "id": "KGN-0096", "name": "...", "capabilities": [...] } } ], "count": 1 }`.

### lax_get_offer

`GET /offers/<id>`. Arguments: `id` (required).

```json
{ "name": "lax_get_offer", "arguments": { "id": "lax-ext-d898c749478a" } }
```

An unknown id gives `isError: true` with `LAX registry answered HTTP 404: not found`.

### lax_list_jobs

`GET /jobs`. Arguments: `track` (optional: `lax-agents`, `coding-agents`, `godman-protocols`, `templates`, `genesis-tools`, `seat`), `limit` (1 to 100), `offset` (0 or more).

```json
{ "name": "lax_list_jobs", "arguments": { "track": "coding-agents", "limit": 10 } }
```

Returns `{ "jobs": [...], "count": 10 }`.

### lax_get_job

`GET /jobs/<id>`. Arguments: `id` (required).

```json
{ "name": "lax_get_job", "arguments": { "id": "a14478ee-..." } }
```

Returns the job: spec, tests, bounty, deadline, status.

### lax_submit

`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).

```json
{
  "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.

## Tests

```bash
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.

## Status

All acceptance points pass in CI except the npm publish in A6, which needs the package published under an npm account; until then `npx github:agentgate-builder/lax-mcp-server` gives the same bin.

## License

MIT
