12306-mcp publishes a Docker image that installs the package from npm instead of copying it
This is a 12306 ticket search server based on the Model Context Protocol (MCP).
At a glance
- What is it?
- A small Model Context Protocol server exposing China's rail ticket search to assistants, written in TypeScript with two transports and a four row feature table. The build story is where it gets thin: the Docker image installs the published package rather than the source beside it, and the test script starts the server rather than asserting anything.
- Who is it for?
- This is a small, focused server worth reading if you are wiring an assistant to Chinese rail data, and the two documented design documents are the right place to start since the README is thin. Two practical cautions.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 64 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 3, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The Docker image installs the package instead of the source beside it
The Dockerfile is three effective lines. It starts from a Node LTS Alpine image, sets a working directory, and installs the package by name from the registry. There is no copy step.
That means the image built from this repository contains the code published to the package registry, not the code sitting next to the Dockerfile. A contributor who edits the source, builds the image locally to test the change, and runs it will be testing released code.
The run commands in the documentation compound the effect. The interactive one builds the image, then runs the installed entry point inside it, which is the one case where the behaviour is unsurprising. The second builds the image, maps a host port to a container port, runs detached, and passes a port argument to the same entry point, which reinforces that the image is a runtime wrapper rather than a bundle of the local source.
The package manifest does declare the published file list as the build directory alone, so the registry artefact is a compiled tree with no sources in it. Combined with an install-only Dockerfile, there is no path by which the image can carry your local changes.
Two transports from one entry point
The server exposes two ways of being reached. Over standard input and output, which is the transport most assistants use for a local process, the invocation is a single command. Over HTTP, the same entry point is given a port.
The same command-line binary serves both, and the HTTP mode is what the container examples use. A single flag distinguishes them, which keeps the surface small.
The manifest registers that binary under the package name, so it is invocable by name rather than by path once installed. It also declares the module type as ES modules and the entry point as the compiled index in the build directory, which is consistent with a TypeScript project compiled to modern JavaScript and run directly by Node.
The runtime requirement is stated in two places with different numbers. The engines field asks for Node 18 or newer, and the documentation says the server needs Node 18 or newer while noting that the debugging command, which launches the protocol inspector, needs a later major version. So the server and its debugger have different floors, and only the server's floor is the deployment constraint.
The test script compiles and starts the server
There is one test script and two variants, and what they do is worth stating plainly.
Each one compiles the TypeScript and then runs the compiled entry point. There is a variant that starts it on a fixed port and another that launches the protocol inspector against it. No assertions appear anywhere in the manifest.
So the suite verifies that the project compiles and that the server starts. That is a smoke test, and for a server whose whole job is proxying an upstream API it catches the two failures that would otherwise be discovered by a user: a broken build and a process that exits immediately on startup.
It is also consistent with the testing surface implied by the repository, which has no separate test directory in the root listing. A project of this size can reasonably skip a unit test suite, but the naming is what misleads: a reader seeing a script called test will assume assertions, and there are none.
For anyone extending this, that gap is the first thing to fill.
Two overrides pin transitive dependencies
The dependency set is small and modern: the protocol SDK, an HTTP client, a command line parser, a date library with a time zone companion, a schema validator, and one package that provides an HTTP server for the protocol.
What catches the eye is the override block. Two transitive dependencies are forced to specific versions. The first pins the protocol SDK inside the HTTP server package to the same range the project itself uses, which keeps two copies of the same library from ending up in the tree. That is a reasonable thing to do and a common source of confusion when it is missing.
The second pins a routing library inside a dependency named for routing to a much later major version than it would otherwise resolve to. Overrides of this kind exist because an upstream dependency has not yet widened its own constraint, and they are the first thing to re-check when anything upstream releases: a forced major can break silently in either direction.
Neither override is explained in a comment, so a maintainer inheriting this manifest has to infer the intent from the version numbers alone.
The build scripts branch by operating system
The manifest uses a small utility to run different scripts depending on the platform, and applies it before and after the compile step.
Before compiling, one script clears the build directory using a Windows command and another uses the portable shell equivalent. After compiling, the macOS and Linux branch adds the execute permission to the compiled entry point, and the fallback for other platforms does nothing. The compile step itself is a plain TypeScript build with no branching.
The reason for the permission step is the executable bit, and it matters because the package registers a binary. A published binary without the execute bit fails on Unix with a permission error rather than a helpful message, so making the build produce it is the right place to solve it.
Two things follow for a reader. A Windows contributor runs the build natively without a shell, which is what the branching is for, and a contributor on any other platform will notice that the permission step only exists for two of them. The fallback being an empty string rather than an error means a platform without a defined branch silently produces a binary that may not be executable.
Three query modes are complete and the rest are explicitly not planned
The feature table is a status list with five rows, and four of them are marked done.
Ticket search is complete. Filtering train information is complete. Querying for intermediate stations between two places is complete. Querying for transfers, meaning itineraries that change trains, is complete. The fifth row is the interesting one: the remaining interfaces are marked as planned, with a note inviting feature requests.
So the server's scope is a read only search surface with four capabilities, and the table is explicit that nothing beyond those is in progress. That is worth more than a longer list would be, because a caller can tell from the table which tools are safe to depend on.
The three query modes are also the interesting engineering. Searching for trains on a route is one thing; answering what stations a train passes through, or finding an itinerary that changes trains, requires joining or reasoning across results in a way a plain lookup does not. The page links a document explaining how the service works internally and another containing the architecture diagram, which is where those details belong, since the README says almost nothing about them.
The project is a study, and its documentation lives outside the README
Two documents are linked as the real documentation: one explaining the working principle of the service, and one holding the architecture diagram. Neither is summarised in the README, which is roughly two hundred words of status table and commands.
The framing of the project is stated in one line near the end: it is for study only, with an invitation to hurry the updates along. That is an unusually candid line, and it frames everything else on the page, including the planned row in the feature table.
The page also points at a separate repository for a skill wrapper, so the intended usage looks like an assistant plus a skill layer plus this server rather than the server alone. Two registry badges link the project to external MCP directories, which is how servers of this kind are usually discovered.
The repository itself is small and conventional: a source directory, a build directory, documentation, the Docker file, a formatting configuration, a registry metadata file, and the manifest. There is no test directory, no continuous integration configuration in the root listing, and no contribution guide, which is consistent with a single author project of this size.
Editorial conclusion
This is a small, focused server worth reading if you are wiring an assistant to Chinese rail data, and the two documented design documents are the right place to start since the README is thin. Two practical cautions. The container image installs the published package rather than the checkout beside it, so an image built from a modified source tree still runs released code, which defeats the purpose of building locally. And the dependency set pins a router transitive package through an override, which is the kind of line that should be re-checked when anything upstream moves. Treat the feature table as the real scope: three query modes are complete and the rest are explicitly unplanned.
Frequently asked questions
What is 12306-mcp and what can it query?
It is a Model Context Protocol server exposing rail ticket search. The feature table marks four capabilities complete: ticket search, filtering train information, querying intermediate stations between two places, and querying for transfers. Remaining interfaces are marked as planned rather than in progress.
How do I run 12306-mcp?
Over standard input and output it runs as a single command, and over HTTP the same entry point takes a port flag. The package registers a binary under its own name, and the documented runtime requirement is Node 18 or newer.
Does building the 12306-mcp Docker image use my local source?
No. The Dockerfile starts from a Node Alpine image and installs the package by name from the registry, with no copy step, so an image built from a modified checkout still runs the published code. The package itself ships only its compiled build directory.
What does the 12306-mcp test script check?
It compiles the TypeScript and starts the compiled server, with a variant that binds a fixed port and another that launches the protocol inspector. There are no assertions, so it verifies that the project builds and that the process starts.
How do I configure 12306-mcp in an MCP client?
Add an entry to the servers map in your client configuration, giving the command as npx with arguments that pass a yes flag and the package name. That runs it over standard input and output as a local process.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/joooook-12306-mcp)