lax-mcp-server / tree / src/server.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// LAX registry as MCP tools. createLaxMcpServer returns an McpServer that is not yet connected.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';

export const DEFAULT_BASE_URL = 'https://lax.kognai.ai/registry';
export const VERSION = '0.1.0';
export const TRACKS = ['lax-agents', 'coding-agents', 'godman-protocols', 'templates', 'genesis-tools', 'seat'] as const;

export type Logger = (line: string) => void;

export interface LaxMcpOptions {
  baseUrl?: string;
  fetch?: typeof globalThis.fetch;
  /** Receives one line per tool call: tool name, HTTP status and duration only. Never arguments or headers. */
  log?: Logger;
  /** Per request timeout in milliseconds (default 20000). */
  timeoutMs?: number;
}

const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PAYOUT_RE = /^0x[0-9a-fA-F]{40}$/;
const DID_RE = /^did:[a-z0-9]+:.+/;

const text = (value: unknown, isError = false): CallToolResult => ({
  content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
  ...(isError ? { isError: true } : {}),
});
const refuse = (reason: string): CallToolResult => text(`Refused before any request: ${reason}`, true);

function isHttpsUrl(s: string): boolean {
  try {
    const u = new URL(s);
    return u.protocol === 'https:' && !!u.hostname;
  } catch {
    return false;
  }
}

export function createLaxMcpServer(options: LaxMcpOptions = {}): McpServer {
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
  const doFetch = options.fetch ?? globalThis.fetch;
  const log: Logger = options.log ?? (() => {});
  const timeoutMs = options.timeoutMs ?? 20000;

  async function call(tool: string, method: 'GET' | 'POST', path: string, body?: unknown): Promise<CallToolResult> {
    const started = Date.now();
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      const res = await doFetch(baseUrl + path, {
        method,
        headers: body === undefined ? { accept: 'application/json' } : { accept: 'application/json', 'content-type': 'application/json' },
        body: body === undefined ? undefined : JSON.stringify(body),
        signal: ctrl.signal,
      });
      const raw = await res.text();
      let parsed: unknown = raw;
      try {
        parsed = raw ? JSON.parse(raw) : null;
      } catch {
        // not JSON: keep the text
      }
      log(`${tool} ${res.status} ${Date.now() - started}ms`);
      if (!res.ok) {
        const msg = parsed && typeof parsed === 'object' && 'error' in parsed ? String((parsed as { error: unknown }).error) : typeof parsed === 'string' && parsed ? parsed.slice(0, 500) : res.statusText || 'no message';
        return text(`LAX registry answered HTTP ${res.status}: ${msg}`, true);
      }
      const result = text(parsed);
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) result.structuredContent = parsed as Record<string, unknown>;
      return result;
    } catch (e) {
      const reason = ctrl.signal.aborted ? `no answer within ${timeoutMs} ms` : e instanceof Error ? e.message : String(e);
      log(`${tool} network-error ${Date.now() - started}ms`);
      return text(`LAX registry request failed (network): ${reason}`, true);
    } finally {
      clearTimeout(timer);
    }
  }

  const server = new McpServer({ name: 'lax-mcp', version: VERSION });

  server.registerTool(
    'lax_search_agents',
    {
      title: 'Search LAX agents',
      description: 'Search agent offers listed on the LAX registry by free text (name, skill, category). Free, no key. Returns {results, count}.',
      inputSchema: { query: z.string().max(100).optional().describe('Free text, at most 100 characters. Omit to list offers.') },
    },
    async ({ query }) => {
      if (query !== undefined && query.length > 100) return refuse('query is longer than 100 characters');
      const qs = query ? `?query=${encodeURIComponent(query)}` : '';
      return call('lax_search_agents', 'GET', `/search${qs}`);
    },
  );

  server.registerTool(
    'lax_get_offer',
    {
      title: 'Get one LAX offer',
      description: 'Read one agent offer by its LAX id (for example KGN-0096 or lax-ext-...): capabilities, pricing, how to hire.',
      inputSchema: { id: z.string().min(1).regex(ID_RE).describe('The offer id.') },
    },
    async ({ id }) => {
      if (!id) return refuse('id is required');
      if (!ID_RE.test(id)) return refuse('id has characters an offer id never has');
      return call('lax_get_offer', 'GET', `/offers/${encodeURIComponent(id)}`);
    },
  );

  server.registerTool(
    'lax_list_jobs',
    {
      title: 'List LAX jobs',
      description: 'List open jobs Kognai pays for (bounties and swarm seats), optionally by track. Returns {jobs, count}.',
      inputSchema: {
        track: z.enum(TRACKS).optional().describe('Job track.'),
        limit: z.number().int().min(1).max(100).optional().describe('1 to 100, default 50.'),
        offset: z.number().int().min(0).optional().describe('0 or more, default 0.'),
      },
    },
    async ({ track, limit, offset }) => {
      if (track !== undefined && !(TRACKS as readonly string[]).includes(track)) return refuse(`track must be one of ${TRACKS.join(', ')}`);
      if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 100)) return refuse('limit must be a whole number from 1 to 100');
      if (offset !== undefined && (!Number.isInteger(offset) || offset < 0)) return refuse('offset must be a whole number of 0 or more');
      const qs = new URLSearchParams();
      if (track !== undefined) qs.set('track', track);
      if (limit !== undefined) qs.set('limit', String(limit));
      if (offset !== undefined) qs.set('offset', String(offset));
      const q = qs.toString();
      return call('lax_list_jobs', 'GET', `/jobs${q ? `?${q}` : ''}`);
    },
  );

  server.registerTool(
    'lax_get_job',
    {
      title: 'Get one LAX job',
      description: 'Read one job by id: spec, tests, bounty, deadline, status and bid window.',
      inputSchema: { id: z.string().min(1).regex(ID_RE).describe('The job id (a UUID).') },
    },
    async ({ id }) => {
      if (!id) return refuse('id is required');
      if (!ID_RE.test(id)) return refuse('id has characters a job id never has');
      return call('lax_get_job', 'GET', `/jobs/${encodeURIComponent(id)}`);
    },
  );

  server.registerTool(
    'lax_submit',
    {
      title: 'Submit work for a LAX job',
      description: 'Submit finished work for an open job (AI agents only, free, at most 3 a day per job and contact). The contact is never shown publicly and never logged.',
      inputSchema: {
        job_id: z.string().min(1).regex(ID_RE).describe('The job id.'),
        builder: z.string().min(2).max(80).describe('Builder name, 2 to 80 characters.'),
        contact: z.string().min(1).describe('How Kognai reaches you (email). Never shown publicly.'),
        repo_or_patch_url: z.string().url().regex(/^https:\/\//).describe('https link to the repository at a commit, or to a patch.'),
        payout_address: z.string().regex(PAYOUT_RE).describe('Base address for the USDC payout: 0x and 40 hex characters.'),
        agent_did: z.string().regex(DID_RE).describe('The submitting agent DID, for example did:pkh:eip155:8453:<address>.'),
        notes: z.string().max(2000).optional().describe('Optional notes, at most 2000 characters.'),
      },
    },
    async (a) => {
      const missing = (['job_id', 'builder', 'contact', 'repo_or_patch_url', 'payout_address', 'agent_did'] as const).filter((k) => !a[k]);
      if (missing.length) return refuse(`missing ${missing.join(', ')}`);
      if (!ID_RE.test(a.job_id!)) return refuse('job_id has characters a job id never has');
      if (a.builder!.length < 2 || a.builder!.length > 80) return refuse('builder must be 2 to 80 characters');
      if (!PAYOUT_RE.test(a.payout_address!)) return refuse('payout_address must be 0x followed by 40 hex characters');
      if (!isHttpsUrl(a.repo_or_patch_url!)) return refuse('repo_or_patch_url must be an https link');
      if (!DID_RE.test(a.agent_did!)) return refuse('agent_did must look like did:<method>:<id>');
      if (a.notes !== undefined && a.notes.length > 2000) return refuse('notes are longer than 2000 characters');
      const body: Record<string, string> = { builder: a.builder!, agent_did: a.agent_did!, contact: a.contact!, repo_or_patch_url: a.repo_or_patch_url!, payout_address: a.payout_address! };
      if (a.notes) body.notes = a.notes;
      return call('lax_submit', 'POST', `/jobs/${encodeURIComponent(a.job_id!)}/submissions`, body);
    },
  );

  return server;
}

agentgate-source: code mirror on Cloudflare Pages. All repos