coding-challenges
by arclops
README.md
# mcp-server
[](https://github.com/arclops/mcp-server/actions/workflows/ci.yml)



A Model Context Protocol server that lets an AI coding agent find out whether
anyone has shared a solution to a [Coding Challenge](https://codingchallenges.fyi/),
and in which language.
The protocol is implemented from the specification rather than through an SDK:
JSON-RPC 2.0 over stdio, one compact message per line, in Go with **no
dependencies outside the standard library**.
Built as a solution to
[Coding Challenge #104 — MCP Server for AI Agents](https://codingchallenges.fyi/challenges/challenge-mcp-server/).
Its verdict on the official SDK is the interesting part:
```
$ node interop/client.mjs ./mcp-server
PASS initialize completed
PASS tools/list returned every tool
PASS the finder declares an output schema
PASS an unknown tool is an invalid-params error
PASS bad arguments are an invalid-params error
PASS a miss is not reported as an error
...
25 passed, 0 failed
```
## Connecting an agent
```json
{
"mcpServers": {
"coding-challenges": { "command": "mcp-server" }
}
}
```
`go install github.com/arclops/mcp-server@latest`, then point the client at the
binary with no arguments. Diagnostics go to stderr; stdout carries only protocol
messages, because anything else on it corrupts the connection.
## The tools
| Tool | Arguments | Returns |
| --- | --- | --- |
| `hello` | `name` | `Hello, <name>`. The connectivity check: it proves the handshake, the tool listing and a call all work before anything depends on the network |
| `CodingChallengesSolutionFinder` | `challenge` | Links to the pages listing shared solutions for the challenge that best matches the name |
| `CodingChallengesFindSolution` | `challenge`, `language` | The individual solutions for one challenge, filtered to one language |
| `CodingChallengesListChallenges` | — | Every challenge that has shared solutions, with links |
Asked for solutions to `wc` written in Go, against the real repository:
```
Build your own wc Tool has 159 shared solution(s); 42 are written in Go.
https://github.com/CodingChallengesFYI/SharedSolutions/blob/main/Solutions/challenge-wc.md
1. wc-tool by andrenbrandao
https://github.com/andrenbrandao/wc-tool
2. wc-go by praveshdev3
https://github.com/praveshdev3/wc-go
```
## Watching the protocol
`mcp-server inspect` performs the conversation the MCP Inspector performs —
initialize, the initialized notification, tools/list and some tool calls — and
prints the actual JSON-RPC in both directions:
```
$ mcp-server inspect
connection: stdio, one JSON-RPC message per line
--> {
"id": 1,
"jsonrpc": "2.0",
"method": "initialize",
"params": { "protocolVersion": "2025-11-25", "capabilities": {}, ... }
}
<-- {
"id": 1,
"jsonrpc": "2.0",
"result": {
"protocolVersion": "2025-11-25",
"capabilities": { "tools": {} },
"serverInfo": { "name": "coding-challenges", ... },
"instructions": "This server reads the Coding Challenges ..."
}
}
```
`-challenge NAME -language NAME` adds the two lookups, so the whole thing can be
demonstrated in one command:
```
$ mcp-server inspect -challenge bitcaskk
...
No challenge matching "bitcaskk" is listed in the shared solutions repository.
Closest titles:
- Build Your Own Bitcask
```
## The protocol, by hand
| Concern | What this server does |
| --- | --- |
| Transport | stdio, newline delimited JSON-RPC. A message must not contain an embedded newline, and a final message with no newline after it is still read |
| Revision | `2025-11-25`, with `2025-06-18` also spoken. Anything else is answered with `2025-11-25` and the client decides whether to continue |
| Lifecycle | `initialize` → result, then `notifications/initialized`. The client counts as initialized once it has been told how to talk to us, so a client that skips the notification still works |
| Capabilities | `tools` only. Nothing is claimed that is not implemented |
| Requests | `ping`, `tools/list`, `tools/call` |
| Notifications | Never answered, not even with an error: the client cannot correlate a reply with anything |
| Unknown method | `-32601` |
| Unparseable message | `-32700` with a **null id**, and batched arrays are refused with `-32600` rather than half-answered |
| Unknown tool, or arguments that do not match the tool's schema | `-32602`. The call was never valid, so it is not a result |
| A tool that fails while doing its job | A **result** with `isError: true`, because the model should read it and react rather than see a broken connection |
| A challenge that is not listed | Not an error either: an answer, with the closest titles to try |
| A panicking tool | Recovered, reported as a failed call, and the connection stays up |
| Message size | Capped, so a malformed stream cannot make the server allocate without bound |
Every tool declares an `outputSchema` and returns matching `structuredContent`,
alongside a text block written for a model to read. The tools are annotated
`readOnlyHint: true` because none of them changes anything.
## Design notes
- **The protocol is implemented, not imported.** There is no Go SDK in the
dependency list, so the transport framing, the lifecycle, the error codes and
the negotiated revision are all visible in a few hundred lines of this
repository rather than behind a library. The check that matters is not "does
it compile against a library" but "does the official SDK accept what it says",
which is what `interop/` answers.
- **The instruction field is used for what it is for.** Clients may put
`instructions` into the model's context, so it explains how the three finder
tools fit together instead of describing each one twice.
- **"Not found" is an answer, not an error.** A model that mistypes a challenge
name gets the closest titles and can retry in the same turn. Turning that into
a failed call would make it give up or apologise.
- **The parsers are tolerant on purpose.** The repository is written by hand by
hundreds of contributors: rows omit the closing pipe, row numbers repeat,
authors are plain text instead of links, TypeScript is spelled four ways and
JavaScript appears as `JacaScript`. All of that is in the test fixtures, taken
from the real files rather than invented.
- **Pages are cached for five minutes.** A conversation with an agent asks the
same question repeatedly; hammering GitHub for it would be rude and slow.
- **An empty table is not a missing table.** A page whose table has no rows is a
challenge nobody has solved; a page with no table at all means the format
changed, and that is reported as an error.
- **Language names are folded**: `js`, `node`, `node.js` and `JacaScript` all
mean JavaScript, so a request for `golang` matches a row saying `Go`.
## Testing
```bash
go test ./... # 87 tests, 86-96% coverage per package
go test ./... -race
node interop/client.mjs ./mcp-server # the official SDK, fixture data
node interop/client.mjs ./mcp-server --live # the official SDK, real GitHub
```
- **Protocol tests** drive a server over pipes exactly as stdio does, and cover
the handshake, version negotiation, the catalogue, every tool call, unknown
methods, malformed JSON, batch arrays, blank lines, notifications that must
not be answered, a panicking tool, and closing the input to shut down.
- **Parser tests** run against the real repository files kept in testdata: a
real README, a real solutions page with more than 150 rows, and a real
one-row page. They assert the awkward rows specifically, because those are the
ones that break.
- **Finder tests** use an `httptest` server, so the suite is deterministic and
offline. They cover caching and its expiry, connection failures, 404s, empty
documents, a page with no table, and context cancellation.
- **Interoperability** is checked against the official TypeScript SDK, which
validates every reply against its own schemas. It runs in CI on every push.
- **The binary is tested as a binary**: one test builds it, spawns it and drives
it through real pipes, and another asserts that stdout carries nothing but
protocol messages even with logging on.
## Challenge steps
| Step | Requirement | Where |
| --- | --- | --- |
| 1 | A `hello` tool taking a string and returning `Hello, <string>` | `internal/tools` |
| 2 | Driven from the MCP Inspector | `mcp-server inspect` prints the same conversation |
| 3 | `CodingChallengesSolutionFinder` reading the shared solutions README | `internal/challenges/parse.go`, `finder.go` |
| 4 | Handle GitHub being unreachable, the format changing, and a challenge not being listed | `finder.go` errors, `ParseSolutions`, suggestions |
| 5 | Added to an AI coding agent | see the client configuration above |
| 6 | Solutions for a challenge in a particular language | `CodingChallengesFindSolution` |
| Going further | More tools | `CodingChallengesListChallenges` |
## Limitations
- **No commercial agent is installed in the development environment**, so step 5
is covered by the configuration snippet and by driving the same stdio
transport with the official SDK rather than by a screenshot from an editor.
- Only the stdio transport is implemented. Streamable HTTP is a larger surface
(sessions, resumability, authorization) and half of it would be worse than
none.
- No resources or prompts: the server declares only the `tools` capability.
- The tool list is fixed for the lifetime of the process, so `listChanged` is
false and no list-changed notification is ever sent.
## License
MIT. See [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues