EL

ecko-lang/mcp

Developer tools
2 stars 0 forks Quality 59 Trend 28MIT

Model Context Protocol for Ecko, both directions: call an MCP server's tools, or be one.

Overview

Model Context Protocol, both directions. Call an MCP server's tools, or be one. MCP is JSON-RPC 2.0 with a fixed vocabulary: a handshake, then tools/, resources/ and prompts/*. MCP goes both directions: this package speaks to a server, and lets you be one. Register it with a client by pointing it at ecko your_server.ecko. is how almost every MCP server ships: the client starts the process and uses the pipes it already has, so there is no port and nothing to authenticate. Needs . is the shape a hosted server takes. Needs . is a transport backed by a function, for tests. Needs no capability at all. offer is { tools, resources, prompts }, each optional. A bare list is taken as the tools.

README

mcp

Model Context Protocol, both directions. Call an MCP server’s tools, or be one.

MCP is JSON-RPC 2.0 with a fixed vocabulary: a handshake, then tools/*, resources/* and prompts/*.

Install

ecko get github.com/ecko-lang/mcp

Usage

MCP goes both directions: this package speaks to a server, and lets you be one.

Calling a server

import mcp

session = mcp.connect(mcp.stdio_transport("npx", ["-y", "@some/mcp-server"]))

for t in mcp.tools(session) {
    print("{t.name}: {t.description}")
}

print(mcp.text_of(mcp.call_tool(session, "search", { query: "ecko" })))
mcp.close(session)

Hand the tools straight to a model:

answer = ai "what changed in the changelog?" using mcp.as_tools(session)

Being a server

import mcp

mcp.serve_stdio([
    mcp.tool(
        "word_count",
        "Count the words in some text",
        { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
        fn(args) len(split(trim(args.text), " ")),
    ),
], { name: "words", version: "1.0" })

Register it with a client by pointing it at ecko your_server.ecko.

Transports

stdio is how almost every MCP server ships: the client starts the process and uses the pipes it already has, so there is no port and nothing to authenticate. Needs exec.

mcp.stdio_transport("npx", ["-y", "@some/mcp-server"], { timeout_ms: 30000 })

HTTP is the shape a hosted server takes. Needs net.

token = secret(os.env("MCP_TOKEN"))
mcp.http_transport("https://mcp.example.com/", {
    headers: { Authorization: "Bearer {reveal(token)}" },
})

mcp.fake(handler) is a transport backed by a function, for tests. Needs no capability at all.

API

Client

call result
mcp.connect(transport, info?) Handshake and return a session.
mcp.close(session) Close the transport.
mcp.server_version(session) / mcp.server_capabilities(session) What the server answered with.
mcp.tools(session) Every tool, following pagination.
mcp.call_tool(session, name, args?) The raw result: content, maybe isError.
mcp.text_of(result) / mcp.is_error(result) The text parts; whether the tool failed.
mcp.as_tools(session) / mcp.as_tool(session, t) Tool specs for ai ... using.
mcp.resources(session) / mcp.read_resource(session, uri) Resources.
mcp.prompts(session) / mcp.get_prompt(session, name, args?) Prompts.
mcp.cancel(session, id, reason?) / mcp.next_request_id(session) Cancel a request.
mcp.call(session, method, params?) / mcp.notify(...) Anything not wrapped above.
mcp.request(id, method, params?) / mcp.notification(method, params?) / mcp.encode(message) Build and encode a raw JSON-RPC message.

Server

call result
mcp.serve_stdio(offer, info?) / mcp.serve_http(port, offer, info?) Serve. Blocks.
mcp.tool(name, description, schema, handler) Declare a tool.
mcp.resource(uri, name, mime, reader) Declare a resource. Read lazily.
mcp.prompt(name, description, arguments, renderer) Declare a prompt.
mcp.handle(request, offer, info?) Answer one decoded request (initialize, ping, and the tools, resources and prompts methods). null for a notification.
mcp.handle_text(text, offer, info?) Decode, answer, re-encode.
mcp.text(value) / mcp.tool_error(message) Build content; report a tool failure.
mcp.progress(token, done, total?) / mcp.progress_token(request) Progress notifications.
mcp.tools_changed() / mcp.resources_changed() / mcp.prompts_changed() Announce a change.

offer is { tools, resources, prompts }, each optional. A bare list is taken as the tools.

Constants

name what it is
mcp.PROTOCOL_VERSION The MCP revision this package speaks, shared by both sides so a handshake cannot disagree with itself.
mcp.PAGE_SIZE How many entries a served */list returns per page.
mcp.MAX_PAGES How many pages of a */list the client follows before giving up on a server that never stops paginating.

Two kinds of failure, and they are different

  • A JSON-RPC error means the call was rejected: no such tool, bad argument, server fault. It raises an Ecko error with kind: "mcp", carrying the server’s own code and message.
  • isError on a successful result means the call worked and the tool failed. MCP models this as data on purpose so a model can read the failure and try something else. Check it with mcp.is_error(result).

Conflating them tells a model its arguments were wrong when the tool simply does not exist.

Notes

Lists are paginated. tools, resources and prompts follow nextCursor to the end. Reading the first page and stopping is the worst kind of bug: a short list, and nothing saying it was truncated. A server that repeats a cursor is refused rather than looped on.

Request ids never repeat, and every reply is checked against the id that asked for it. A transport that interleaves messages would otherwise attribute someone else’s result to your call.

A notification is written but not read back. Reading after one would block until the timeout and then take the next reply, misaligning every later call.

Capabilities are computed, not asserted. A server advertises only the sections it actually declared, because a client branches on that.

A missing list reads as empty. A server with no tools may answer {}.

The HTTP transport sends its body and headers since 0.55.1. Before that it passed both positionally to http.post, which read neither, so every request went out empty and unauthenticated - a hosted server would have refused it.

Not implemented

Server-to-client requests (sampling, roots, elicitation), resource subscriptions, and the OAuth flow for hosted servers. Everything absent fails loudly rather than silently.

Testing

ecko test

56 cases, all offline against mcp.fake - no socket, no subprocess, no API key.

License

MIT

View the full README on GitHub

Install

This server does not publish a one-line install command.

Open the repository installation guide New to MCP? Start with the mcpskill.org guide →