Skip to content

Commit d65842e

Browse files
authored
Merge pull request #280 from modelcontextprotocol/chore/agents-md-makefile
Add AGENTS.md, a Makefile and lint checks in CI
2 parents 94626a7 + 4671a9f commit d65842e

6 files changed

Lines changed: 176 additions & 9 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
uses: actions-rust-lang/setup-rust-toolchain@v2
4040
with:
4141
toolchain: stable
42+
components: clippy, rustfmt
4243

4344
- name: Set up Go
4445
uses: actions/setup-go@v7
@@ -52,5 +53,8 @@ jobs:
5253
with:
5354
ruby-version: "3.4"
5455

55-
- name: Run smoke tests
56-
run: ./tests/smoke-test.sh
56+
- name: Lint
57+
run: make lint
58+
59+
- name: Build and run smoke tests
60+
run: make test

‎AGENTS.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# AGENTS.md
2+
3+
Guidance for anyone changing these examples, human or agent. The [README](./README.md) covers what is here and how to run it.
4+
5+
## What this repository is for
6+
7+
These are the companion examples for two tutorials on modelcontextprotocol.io: [Build an MCP server](https://modelcontextprotocol.io/docs/develop/build-server) and [Build an MCP client](https://modelcontextprotocol.io/docs/develop/build-client). The tutorials quote this code, so readers meet it line by line. Every change should leave the code easier to read, not just more complete.
8+
9+
## Keep the examples minimal
10+
11+
- Show the protocol, not production engineering. No retries, caching, config layers, logging frameworks or helper abstractions unless a tutorial teaches them.
12+
- Prefer the SDK's most direct, idiomatic API over a clever or general one.
13+
- Add a feature only when it shows something a reader needs to know about MCP. If it would only make the example more robust, leave it out.
14+
- Keep dependencies to the MCP SDK, the Anthropic SDK and what the language needs to make an HTTP call or read `.env`.
15+
16+
## Comments
17+
18+
Explanations belong in the tutorial pages and the example READMEs, not in the source. Keep comments to a minimum: add one only where the code would otherwise look wrong or surprising, such as a workaround for an SDK limitation, and keep it to a line or two.
19+
20+
## Keep the five languages in sync
21+
22+
The Python, TypeScript, Go, Rust and Ruby versions of each example should behave the same way.
23+
24+
- A behavior change goes into all five in the same pull request. If one language can't follow, say why in the pull request and in that example's README.
25+
- Differences that follow a language's conventions are fine (for example, `get-alerts` in TypeScript, `get_alerts` elsewhere). Differences in behavior are not.
26+
- Update every affected README: each one documents its own example, and the root README describes what all of them share.
27+
28+
## Keep the tutorials in sync
29+
30+
The tutorials live in [`modelcontextprotocol/modelcontextprotocol`](https://github.com/modelcontextprotocol/modelcontextprotocol), under `docs/docs/<protocol version>/develop/build-server.mdx` and `build-client.mdx`. When a change touches code a tutorial quotes or describes, open a matching pull request there and link the two.
31+
32+
## Checks and tests
33+
34+
Run `make check` before opening a pull request. It runs the linters and formatters, rebuilds every example and runs the smoke tests. `make help` lists the per-language targets, such as `make lint-go`, for when you only have some toolchains installed.
35+
36+
| Language | Checks |
37+
|------------|-------------------------------------------------|
38+
| Python | `ruff check`, `ruff format --check` |
39+
| TypeScript | `tsc --noEmit` |
40+
| Go | `gofmt -l`, `go vet` |
41+
| Rust | `cargo fmt --check`, `cargo clippy -D warnings` |
42+
| Ruby | `ruby -wc`, failing on warnings |
43+
44+
`make fmt` fixes formatting for Python, Go and Rust.
45+
46+
- A change to client behavior needs matching changes to the mock MCP server and the tool-loop test in [`tests/helpers`](./tests/helpers), so CI exercises the new behavior in all five clients. See [`tests/README.md`](./tests/README.md).
47+
- Tests never call a real Anthropic API. Server tests may reach the National Weather Service API, and treat it being unreachable as a skip.
48+
49+
## Dependencies
50+
51+
Pin to released SDK versions, and commit the lockfiles. Dependabot opens the routine upgrades. When an SDK upgrade changes how an example has to be written, update all five languages and the tutorials as above.
52+
53+
## Pull requests
54+
55+
Follow the Contributing section of the [README](./README.md#contributing). In particular, say in the pull request if AI assistance was used.

‎Makefile‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Lint, format, build and test every example. `make check` is what CI runs.
2+
# Each per-language target needs only that language's toolchain.
3+
4+
PYTHON_DIRS := weather-server-python mcp-client-python
5+
TS_DIRS := weather-server-typescript mcp-client-typescript tests/helpers
6+
GO_DIRS := weather-server-go mcp-client-go
7+
RUST_DIRS := weather-server-rust mcp-client-rust
8+
RUBY_FILES := weather-server-ruby/weather.rb mcp-client-ruby/client.rb
9+
10+
.PHONY: help check lint fmt build test \
11+
lint-python lint-typescript lint-go lint-rust lint-ruby \
12+
fmt-python fmt-go fmt-rust \
13+
build-typescript build-go build-rust
14+
15+
help:
16+
@echo "make check lint, then build and run the smoke tests"
17+
@echo "make lint check formatting and lint every example"
18+
@echo "make fmt format Python, Go and Rust in place"
19+
@echo "make build rebuild the TypeScript, Go and Rust examples"
20+
@echo "make test rebuild, then run tests/smoke-test.sh"
21+
@echo "Per language: lint-{python,typescript,go,rust,ruby},"
22+
@echo " fmt-{python,go,rust}, build-{typescript,go,rust}"
23+
24+
# Lint first, then test, even under `make -j`: both install npm packages.
25+
check: lint
26+
$(MAKE) test
27+
28+
lint: lint-python lint-typescript lint-go lint-rust lint-ruby
29+
30+
fmt: fmt-python fmt-go fmt-rust
31+
32+
build: build-typescript build-go build-rust
33+
34+
# smoke-test.sh builds only what is missing, so rebuild first to avoid
35+
# testing stale binaries.
36+
test: build
37+
./tests/smoke-test.sh
38+
39+
lint-python:
40+
@set -e; for d in $(PYTHON_DIRS); do \
41+
echo "== ruff $$d"; \
42+
uv --directory $$d run --quiet ruff check .; \
43+
uv --directory $$d run --quiet ruff format --check .; \
44+
done
45+
46+
fmt-python:
47+
@set -e; for d in $(PYTHON_DIRS); do \
48+
uv --directory $$d run --quiet ruff check --fix .; \
49+
uv --directory $$d run --quiet ruff format .; \
50+
done
51+
52+
lint-typescript:
53+
@set -e; for d in $(TS_DIRS); do \
54+
echo "== tsc $$d"; \
55+
[ -d $$d/node_modules ] || npm --prefix $$d ci --silent; \
56+
(cd $$d && npx tsc --noEmit); \
57+
done
58+
59+
build-typescript:
60+
@set -e; for d in $(TS_DIRS); do \
61+
[ -d $$d/node_modules ] || npm --prefix $$d ci --silent; \
62+
npm --prefix $$d run --silent build; \
63+
done
64+
65+
lint-go:
66+
@set -e; for d in $(GO_DIRS); do \
67+
echo "== gofmt, go vet $$d"; \
68+
unformatted=$$(cd $$d && gofmt -l .); \
69+
if [ -n "$$unformatted" ]; then echo "needs gofmt: $$unformatted"; exit 1; fi; \
70+
(cd $$d && go vet ./...); \
71+
done
72+
73+
fmt-go:
74+
@set -e; for d in $(GO_DIRS); do (cd $$d && gofmt -w .); done
75+
76+
# The binary names are the ones smoke-test.sh runs. `go build -o` adds no
77+
# `.exe` on Windows, so add it to replace the binary the smoke tests pick.
78+
build-go:
79+
cd weather-server-go && go build -o server$$(go env GOEXE) .
80+
cd mcp-client-go && go build -o mcp-client-go$$(go env GOEXE) .
81+
82+
lint-rust:
83+
@set -e; for d in $(RUST_DIRS); do \
84+
echo "== rustfmt, clippy $$d"; \
85+
(cd $$d && cargo fmt --check && cargo clippy --quiet --all-targets -- -D warnings); \
86+
done
87+
88+
fmt-rust:
89+
@set -e; for d in $(RUST_DIRS); do (cd $$d && cargo fmt); done
90+
91+
build-rust:
92+
@set -e; for d in $(RUST_DIRS); do (cd $$d && cargo build --quiet --release); done
93+
94+
# `ruby -wc` exits 0 even when it prints warnings, so fail on the output.
95+
lint-ruby:
96+
@set -e; for f in $(RUBY_FILES); do \
97+
echo "== ruby -wc $$f"; \
98+
out=$$(ruby -wc $$f 2>&1) || { echo "$$out"; exit 1; }; \
99+
echo "$$out"; \
100+
if echo "$$out" | grep -q 'warning:'; then exit 1; fi; \
101+
done

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ Each example only needs its own language toolchain. To work across the whole rep
4040
## Running the tests
4141

4242
```bash
43-
./tests/smoke-test.sh
43+
make check
4444
```
4545

46+
`make check` lints every example, rebuilds the compiled ones and runs `./tests/smoke-test.sh`. Run `make help` for the per-language targets.
47+
4648
The smoke tests verify that every server starts and answers MCP requests (including validating structured tool results against the schemas each tool advertises), and that every client can connect to a mock server and list tools. They run automatically on every pull request via GitHub Actions.
4749

4850
## Contributing
@@ -52,7 +54,7 @@ Contributions are welcome. To submit a change:
5254
1. Fork the repository and create a branch from `main`.
5355
2. Make your change. Keep the examples minimal - they exist to teach the protocol, not to be production services.
5456
3. If you change an example's behavior, setup, or dependencies, update that example's README to match.
55-
4. Run `./tests/smoke-test.sh` and make sure it passes.
57+
4. Run `make check` and make sure it passes. See [`AGENTS.md`](./AGENTS.md) for how to keep the examples minimal and in sync.
5658
5. Open a pull request against `main` describing what changed and why.
5759

5860
A few conventions to follow, in line with the broader [Model Context Protocol contributing guidelines](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/CONTRIBUTING.md):

‎mcp-client-rust/src/main.rs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ struct MessagesResponse {
129129

130130
/// The `type` of a content block, when it has one.
131131
fn block_type(block: &Value) -> &str {
132-
block.get("type").and_then(Value::as_str).unwrap_or_default()
132+
block
133+
.get("type")
134+
.and_then(Value::as_str)
135+
.unwrap_or_default()
133136
}
134137

135138
struct MCPClient {
@@ -359,8 +362,8 @@ impl MCPClient {
359362
let api_key = std::env::var("ANTHROPIC_API_KEY").context("ANTHROPIC_API_KEY is not set")?;
360363
// The base URL is configurable for the same reason the official SDKs
361364
// make it configurable: to point the client at a local stand-in.
362-
let base_url = std::env::var("ANTHROPIC_BASE_URL")
363-
.unwrap_or_else(|_| ANTHROPIC_API_BASE.to_string());
365+
let base_url =
366+
std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| ANTHROPIC_API_BASE.to_string());
364367

365368
let request = MessagesRequest {
366369
model: MODEL_ANTHROPIC,
@@ -460,7 +463,9 @@ async fn main() -> Result<()> {
460463
// Empty counts as unset, as it does in the other clients: `KEY= cmd`
461464
// is how the smoke test forces this path.
462465
if std::env::var("ANTHROPIC_API_KEY").map_or(true, |key| key.is_empty()) {
463-
println!("\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:");
466+
println!(
467+
"\nNo ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key:"
468+
);
464469
println!(" export ANTHROPIC_API_KEY=your-api-key-here");
465470
return Ok(());
466471
}

‎weather-server-rust/src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use rmcp::{
88
transport::stdio,
99
};
1010
use serde::Deserialize;
11-
use serde::de::DeserializeOwned;
1211
use serde::Serialize;
12+
use serde::de::DeserializeOwned;
1313

1414
const NWS_API_BASE: &str = "https://api.weather.gov";
1515
const USER_AGENT: &str = "weather-app/1.0";

0 commit comments

Comments
 (0)