Skip to main content
Glama

kelo-mcp

tests: 18 passing runtime deps: 0 protocol: MCP 2025-06-18 licence: MIT

A small Model Context Protocol server that lets a Claude user ask what their business is owed, who to chase first, and get a chase email drafted in their own voice.

It cannot send anything. Every tool either reads or returns a draft. That is a product decision, not an oversight, and DESIGN.md explains what it costs and what it buys.

Extracted from Kelo, where it backs a live MCP endpoint. No SDK dependency: the JSON-RPC layer is hand-rolled, about 120 lines, and has no runtime dependencies at all.

kelo-mcp request flow

What this demonstrates

A marketer who builds. Hand-rolled a spec-compliant MCP protocol layer with no SDK, a stateless HTTP transport with bearer-key auth and per-workspace scoping, a clean injected backend seam, and 18 tests that run against a stub in under a second, then wrote the judgment calls down in DESIGN.md. Stack: TypeScript, Web-standard Request/Response, Vitest.

Related MCP server: BizDesk MCP

Try it in 30 seconds

npm install
npm run example
curl -s localhost:8787 -H 'authorization: Bearer demo-key' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_weekly_number","arguments":{}}}'
{
  "verified_owed": "£6,050",
  "estimate_at_risk": "£9,600",
  "open_cases": 3,
  "note": "Verified is real overdue invoices. Estimate is quotes and enquiries, and is less certain."
}

That runs against an in-memory demo backend. No database, no Kelo account, no API key beyond the literal string demo-key.

The tools

Tool

Returns

get_weekly_number

Total owed, split into verified invoices and less certain estimates

get_chase_queue

Who to chase first, sorted by priority, value or age

get_customer

One customer's outstanding total, payment history and escalation status

draft_chase

A subject and body for one case. Returns text; never sends

get_due_soon

Invoices due within two weeks, ranked by how likely the client is to pay late

get_insights

Overdue total, average days overdue, aging buckets, worst payers

Wiring it to your own data

The protocol layer knows nothing about invoices. Implement ToolBackend and pass it in:

import { createPostHandler, callTool, TOOL_DEFS, SERVER_INFO, INSTRUCTIONS } from 'kelo-mcp'

export const POST = createPostHandler({
  config: { serverInfo: SERVER_INFO, tools: TOOL_DEFS, instructions: INSTRUCTIONS, callTool },
  resolveKey: async token => {
    const row = await db.apiKeys.findByHash(sha256(token))
    return row && !row.revoked_at ? { workspaceId: row.workspace_id } : null
  },
  makeContext: workspaceId => ({ workspaceId, backend: myBackend }),
})

createPostHandler takes and returns Web-standard Request/Response, so it drops into Next.js route handlers, Cloudflare Workers, Deno, Bun or plain Node 18+ without adaptation.

Layout

src/protocol.ts      JSON-RPC and MCP methods. No product knowledge, no dependencies.
src/tools.ts         Tool definitions: the surface a model actually sees.
src/backend.ts       The seam. Dispatch, formatting, and the ToolBackend interface.
src/demo-backend.ts  In-memory data so the thing runs out of the box.
src/http.ts          Bearer auth and the stateless POST transport.

The split exists so the protocol can be tested without a database. All 18 tests run in well under a second and touch nothing external.

npm test

What is not here

The live Kelo server backs these tools with Postgres queries, a drafting service and a billing entitlement check. That is Kelo's business logic and it is not in this repo. What is here is the protocol layer, the transport, the tool surface, and a demo backend that satisfies the same interface.

Notes on the protocol

Three things the spec asks for that are easy to get subtly wrong, and how this handles them:

  • Notifications get silence, not a response. A message with no id returns null internally and is dropped. If a batch was entirely notifications, the transport answers 202 with an empty body rather than 200 and [].

  • An unknown tool is a tool error, not a protocol error. It comes back as a normal result with isError: true so the model can read it and pick another tool. Reserving -32601 for genuinely unknown methods keeps the two failure modes distinct.

  • The client's protocol version is echoed when it sends one, rather than forcing the server's own.

Licence

MIT

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Enables interaction with the Xero Accounting API to manage contacts, invoices, payments, accounts, and financial reports. It provides a suite of tools for natural language access to accounting records and business performance data.
      20
      1
      Apache 2.0
    • A
      license
      Not graded
      quality
      Not graded
      maintenance
      Provides structured, read-mostly access to small-business back-office data including customers, invoices, and account notes, allowing Claude to query overdue invoices, revenue summaries, and more.
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables AI assistants to query and manage self-hosted accounting data—invoices, balances, and books—through natural language, with read-only tools by default and optional scoped write operations.
      30
      24 PyPI
      1
      AGPL 3.0
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables natural-language access to personal finance data, including consolidated balances, monthly overviews, transaction search, credit card invoices, recurring bills, budgets, category/tag reports, cash flow forecasts, and Open Finance status, with optional read/write tools.
      MIT