Chapter 23 Exercises
These exercises build your fluency with PMCP Skills (SEP-2640). Each one targets a specific skill from the chapter, ordered from mechanical setup (Tier 1) to composition with another advanced feature (Tier 3).
Exercise 1: Register a Single-File Skill (hello-world)
Difficulty: Introductory (10 min)
Practice the mechanical steps of wiring up a single-file skill, with no
supporting references. The goal is to build a binary that registers
skill://hello-world/SKILL.md on a real Server and prints confirmation
of the registered URIs.
Steps:
- Create a new binary project (
cargo new --bin skills-hello) or add a[[bin]]target to an existing crate. - Add
pmcp = { version = "2", features = ["skills", "full"] }toCargo.toml. - Create a skill body string. The minimum body is a
frontmatter-prefixed Markdown document, e.g.:
--- name: hello-world description: Demonstrates the simplest possible MCP skill --- # Hello World Skill When the user greets the agent, respond warmly and offer to help. - Call
pmcp::Server::builder().name("hello-skills").version("0.1.0").skill(Skill::new("hello-world", body)).build()?. - Print a confirmation line that includes the expected registered URIs:
skill://hello-world/SKILL.mdand the auto-synthesizedskill://index.json.
Verify your solution
Run the binary. The exercise passes when the printed output names BOTH
skill://hello-world/SKILL.md AND skill://index.json. If either URI is
missing from the printed list, the registration is incomplete — most
likely you forgot to call .skill(...) on the builder, or you constructed
the Skill with a different name than the one in your frontmatter.
For an executable reference, see
examples/s44_server_skills.rs
and the skill://hello-world/SKILL.md line in its printed output.
Questions to answer:
- What is the default URI for a skill registered with
Skill::new("foo", body)and no.with_path(...)call? - What happens if you call
.skill(Skill::new("foo", ...))twice with the same name on the same builder? (Hint: read.planning/phases/80-sep-2640-skills-support/80-CONTEXT.mdD-5, and experiment withSkills::into_handler()directly to confirm what error surfaces.)
Exercise 2: Register a Multi-File Skill with References (refunds Tier)
Difficulty: Intermediate (25 min)
Build a refunds skill with a SKILL.md body plus one or two
references/*.md supporting files. Demonstrate the §9 invariant: the
references appear in resources/read but NOT in resources/list.
Steps:
- Create three files in your project:
skills/refunds/SKILL.mdskills/refunds/references/policy.mdskills/refunds/references/examples.md
- Use
include_str!to embed each at compile time. - Construct the skill:
let refunds = Skill::new("refunds", REFUNDS) .with_path("acme/billing/refunds") .with_reference(SkillReference::new( "references/policy.md", "text/markdown", POLICY, )) .with_reference(SkillReference::new( "references/examples.md", "text/markdown", EXAMPLES, )); - Register via
.skill(refunds)on the server builder and.build()the server. - Build a
SkillsHandlerseparately (the same skill, but exposed as aResourceHandlerdirectly) so you can drivelistandreadfrom your test code:let handler = Skills::new().add(refunds.clone()).into_handler()?; - Call
handler.list(None, extra.clone()).await?andhandler.read("skill://acme/billing/refunds/references/policy.md", extra.clone()).await?. - Assert: the list contains the SKILL.md URI +
skill://index.jsonbut NOT either reference URI. - Assert: the read of
references/policy.mdreturns the file's body.
Verify your solution
Wrap your assertions in a cargo test (or cargo run with assert! in
main). The exercise solution is verified when BOTH of these assertions
pass simultaneously:
- (a)
resources/listfor the SkillsHandler returns URIs that EXCLUDE everyreferences/*.mdURI. - (b)
resources/readon a reference URI (e.g.skill://acme/billing/refunds/references/policy.md) returns a content body byte-equal to the embedded reference file.
If only (b) passes, you forgot the §9 filter check. If only (a) passes, your read path is wrong — likely a typo in the URI you passed.
For a working reference of the read path (including how to pattern-match
Content::Resource correctly), see
examples/c10_client_skills.rs.
Questions to answer:
- Why does SEP-2640 §9 require this filtering? What attack or UX problem would arise if references were enumerated alongside SKILL.md entries?
- What MIME type would you set for a
.graphqlreference file? (Hint: search the example withgrep -n '\.graphql' examples/s44_server_skills.rsand compare against what your code does.)
Exercise 3: Dual-Surface Bootstrap for a Code-Mode Skill (code-mode Tier)
Difficulty: Advanced (45 min)
Bring it all together. Register the code-mode skill from
examples/skills/code-mode/ (or your own copy) and ALSO publish a prompt
fallback via bootstrap_skill_and_prompt. Build a tiny client driver
that exercises BOTH host flows in the same process and asserts
byte-equality between the concatenated SEP-2640 read results and the
prompt body. That assertion is the runtime proof of the dual-surface
invariant.
Steps:
- Copy or
include_str!examples/skills/code-mode/SKILL.mdand its three references (schema.graphql,examples.md,policies.md) into your project. - Construct the
Skillwith all three references, matching thebuild_code_mode_skill()function inexamples/s44_server_skills.rs. - Register the skill via
bootstrap_skill_and_prompt:let server = pmcp::Server::builder() .name("exercise3") .version("0.1.0") .bootstrap_skill_and_prompt(skill.clone(), "start_code_mode") .build()?; - Retrieve the registered prompt handler via
server.get_prompt("start_code_mode")and invoke.handle(HashMap::new(), extra).await?to obtain the legacy host's prompt body. - Separately, build the SEP-2640 host flow:
let handler = Skills::new().add(skill.clone()).into_handler()?;. Walklistandreadfor SKILL.md plus each reference URI in registration order, concatenating with\n--- {relative_path} ---\nseparators (the same shape asSkill::as_prompt_text()produces). - Assert byte-equality between the two concatenated results:
assert_eq!(sep_2640_text, prompt_text);
Verify your solution
The assert_eq! between the two concatenated byte strings passes. If it
fails, the dual-surface invariant is broken — and your understanding of
what Skill::as_prompt_text() produces is wrong. (This is the exact
assertion examples/c10_client_skills.rs makes; you have a reference
implementation to compare against if you get stuck. The example panics
on failure rather than printing OK — copy that posture in your own
solution.)
Try this: Run cargo run --example c10_client_skills --features skills,full and watch its output. The "Byte-equality assertion" block
at the end is exactly the check your Exercise 3 solution is implementing.
Questions to answer:
- Why does the chapter (and Phase 80) call the byte-equality property "load-bearing"? What silent-failure mode does it prevent on SEP-2640-blind hosts?
- If you registered the
code-modeskill but did NOT callbootstrap_skill_and_prompt(just.skill(skill)), what would change for a host that doesn't support SEP-2640? (Hint:.planning/phases/80-sep-2640-skills-support/80-CONTEXT.mdD-7 and the "pointer-style silent-failure" discussion in the chapter.)
Prerequisites
Before starting these exercises, ensure you have:
- Completed Chapter 23 (Skills) including the dual-surface invariant discussion.
- A working Rust development environment with
pmcp 2.xand theskillsfeature available. - The PMCP examples checked out so you can compare against
s44_server_skills.rsandc10_client_skills.rs.
Next Steps
After completing these exercises, continue to:
- Chapter 22 Exercises -- Code Mode hands-on practice (course ordering: Code Mode appears as Chapter 22, Skills as Chapter 23, per CONTEXT.md D-05).
- Appendix B: Template Gallery -- Production-ready templates including skill-enabled servers.