Skip to content

Add harper-mcp skill: comprehensive guide to Harper's MCP interface#69

Open
kylebernhardy wants to merge 4 commits into
mainfrom
feat/harper-mcp-skill
Open

Add harper-mcp skill: comprehensive guide to Harper's MCP interface#69
kylebernhardy wants to merge 4 commits into
mainfrom
feat/harper-mcp-skill

Conversation

@kylebernhardy

Copy link
Copy Markdown
Member

Fills the gap: no skill covered Harper's MCP interface. Adds harper-mcp/ in the established format (SKILL.md + rules.manifest.yaml + 10 synthesized rules + compiled AGENTS.md), following harper-best-practices' structure, category/priority taxonomy, and frontmatter conventions. npm run validate passes.

Coverage

Category Rules
1. Setup & Connection enabling-mcp (profiles, config surface), connecting-clients (handshake, session/protocol headers, debugging cheat sheet)
2. Tools & Prompts automatic-verb-tools (RBAC-filtered CRUD tools), custom-mcp-tools (incl. the anonymous-exposure security model + gating pattern), custom-mcp-prompts
3. Resources resources-surface (harper://, harper+rest://, subscriptions, list_changed), custom-mcp-resources (templates, {name} vs {+name} + encoded-separator guard, completions, reserved schemes)
4. Operations & Security rate-limiting (session + per-client buckets, identityHeader trust model), durable-quotas (fail-closed hook, race-safe counter guidance), security-posture (hardening checklist)

Content is authored from the current 5.1/5.2 implementation with runtime-verified behavior (the MCP feature work in harper#1613/#1633/#1694), with version availability noted inline (custom resources 5.1.18+, per-client limits and quotas 5.2.0+).

Open item for review

scripts/build.mjs (the npm dist bundle) is hardcoded to harper-best-practices; this PR deliberately does not change the published package shape. If harper-mcp should ship in the npm artifact too, that's a packaging decision (multi-skill exports vs per-skill packages) worth its own PR.

Generated by an LLM (Claude Fable 5).

Ten synthesized rules across four categories: setup & connection
(profiles/config, wire handshake incl. session and protocol-version
header semantics), tools & prompts (automatic RBAC-filtered verb tools,
custom mcpTools with the anonymous-exposure model, mcpPrompts),
resources (harper:// metadata, harper+rest:// descriptors,
subscriptions, custom mcpResources with template semantics and the
encoded-separator guard), and operations & security (session/per-client
rate limiting incl. identityHeader trust model, durable quota hook with
race-safe counter guidance, hardening checklist). Content authored from
the Harper 5.1/5.2 implementation and runtime-verified behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kylebernhardy kylebernhardy requested a review from a team as a code owner July 8, 2026 22:43
@kylebernhardy kylebernhardy requested a review from kriszyp July 8, 2026 22:43

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds comprehensive documentation and rules for Harper's Model Context Protocol (MCP) interface, covering setup, tools, prompts, resources, rate limiting, durable quotas, and security. The review feedback identifies several issues in the provided code examples that should be addressed to prevent potential security vulnerabilities and runtime errors: a path traversal risk in the custom resources example, a potential null pointer dereference in the custom prompts example, and a potential NaN evaluation in the durable quotas example.

Comment on lines +43 to +45
async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
return { text: loadPage(params.path), mimeType: 'text/markdown' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using {+path} in the URI template allows matching across multiple directory segments, including parent directory traversal sequences like ... Passing params.path directly to loadPage without validation can expose the server to a Path Traversal vulnerability. To secure this, add a check to ensure the path does not contain directory traversal sequences.

Suggested change
async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
return { text: loadPage(params.path), mimeType: 'text/markdown' };
}
async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
// Prevent path traversal by ensuring the path does not escape the directory
if (params.path.includes('..')) {
throw new Error('Invalid path');
}
return { text: loadPage(params.path), mimeType: 'text/markdown' };
}

Comment thread harper-mcp/AGENTS.md
Comment on lines +336 to +338
async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
return { text: loadPage(params.path), mimeType: 'text/markdown' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using {+path} in the URI template allows matching across multiple directory segments, including parent directory traversal sequences like ... Passing params.path directly to loadPage without validation can expose the server to a Path Traversal vulnerability. To secure this, add a check to ensure the path does not contain directory traversal sequences.

Suggested change
async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
return { text: loadPage(params.path), mimeType: 'text/markdown' };
}
async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
// Prevent path traversal by ensuring the path does not escape the directory
if (params.path.includes('..')) {
throw new Error('Invalid path');
}
return { text: loadPage(params.path), mimeType: 'text/markdown' };
}

Comment thread harper-mcp/rules/custom-mcp-prompts.md Outdated
Comment on lines +31 to +41
async draftReplyPrompt(args) {
const ticket = await Support.get(args.ticketId);
return {
messages: [
{
role: 'user',
content: { type: 'text', text: `Draft a courteous reply to: ${ticket.body}` },
},
],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If Support.get(args.ticketId) returns null or undefined (e.g., if the ticket is not found), accessing ticket.body will throw a TypeError. Adding a guard check to handle missing records prevents runtime crashes and provides a clearer error message.

Suggested change
async draftReplyPrompt(args) {
const ticket = await Support.get(args.ticketId);
return {
messages: [
{
role: 'user',
content: { type: 'text', text: `Draft a courteous reply to: ${ticket.body}` },
},
],
};
}
async draftReplyPrompt(args) {
const ticket = await Support.get(args.ticketId);
if (!ticket) {
throw new Error('Ticket not found: ' + args.ticketId);
}
return {
messages: [
{
role: 'user',
content: { type: 'text', text: 'Draft a courteous reply to: ' + ticket.body },
},
],
};
}

Comment thread harper-mcp/AGENTS.md Outdated
Comment on lines +239 to +249
async draftReplyPrompt(args) {
const ticket = await Support.get(args.ticketId);
return {
messages: [
{
role: 'user',
content: { type: 'text', text: `Draft a courteous reply to: ${ticket.body}` },
},
],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If Support.get(args.ticketId) returns null or undefined (e.g., if the ticket is not found), accessing ticket.body will throw a TypeError. Adding a guard check to handle missing records prevents runtime crashes and provides a clearer error message.

Suggested change
async draftReplyPrompt(args) {
const ticket = await Support.get(args.ticketId);
return {
messages: [
{
role: 'user',
content: { type: 'text', text: `Draft a courteous reply to: ${ticket.body}` },
},
],
};
}
async draftReplyPrompt(args) {
const ticket = await Support.get(args.ticketId);
if (!ticket) {
throw new Error('Ticket not found: ' + args.ticketId);
}
return {
messages: [
{
role: 'user',
content: { type: 'text', text: 'Draft a courteous reply to: ' + ticket.body },
},
],
};
}

Comment on lines +54 to +55
const existing = await McpQuota.get(id);
const used = existing?.day === today ? existing.used + 1 : 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If existing is found but existing.used is null or undefined, existing.used + 1 will evaluate to NaN. Using a nullish coalescing operator (?? 0) ensures robust defensive programming.

Suggested change
const existing = await McpQuota.get(id);
const used = existing?.day === today ? existing.used + 1 : 1;
const existing = await McpQuota.get(id);
const used = existing?.day === today ? (existing.used ?? 0) + 1 : 1;

Comment thread harper-mcp/AGENTS.md
Comment on lines +449 to +450
const existing = await McpQuota.get(id);
const used = existing?.day === today ? existing.used + 1 : 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If existing is found but existing.used is null or undefined, existing.used + 1 will evaluate to NaN. Using a nullish coalescing operator (?? 0) ensures robust defensive programming.

Suggested change
const existing = await McpQuota.get(id);
const used = existing?.day === today ? existing.used + 1 : 1;
const existing = await McpQuota.get(id);
const used = existing?.day === today ? (existing.used ?? 0) + 1 : 1;

…s for flippable rules

Grounding pass against harper main + merged docs found four fidelity
gaps, all corrected: verb-tool names preserve Resource-path case
(get_Widget, with deterministic collision prefixing); allow/deny globs
are an operations-profile knob (replacing a read-only default), not an
application one — application trimming is exportTypes.mcp + RBAC;
maxTools is the tools/list page size (default 200), not a generation
cap; mcpPrompts entries carry a render(args) function returning the
messages shape, not a method name.

Manifest now declares sources + must_cover for the five rules that map
1:1 to canonical documentation pages (enabling-mcp, resources-surface,
custom-mcp-resources, rate-limiting, durable-quotas), readying the flip
to mode: generate; the remaining five are agent-oriented synthesis with
no single docs source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kylebernhardy

Copy link
Copy Markdown
Member Author

Two follow-ups pushed: (1) a grounding pass against the current implementation caught and fixed four fidelity gaps — verb-tool name casing (get_Widget), allow/deny being operations-profile-only, maxTools being a page size not a cap, and mcpPrompts using a render(args) function rather than a method name; (2) the manifest now declares sources + must_cover for the five rules that map 1:1 to canonical docs pages, so flipping them to mode: generate on the docs-driven pipeline is a one-line manifest edit once a generation run is reviewed. The other five rules are agent-oriented synthesis (debugging cheat sheet, security checklist, gating patterns) with no single docs source and should stay synthesized. Comment generated by an LLM (Claude Fable 5).

Cross-model review follow-ups (Codex + Gemini):
- Register harper-mcp in the SKILLS registry so validate-generated's
  manifest lint and AGENTS.md / SKILL.md round-trip checks actively
  cover it; regenerate both artifacts through the repo renderer so the
  round-trips pass by construction.
- The AGENTS.md lead paragraph moves from a hardcoded constant into the
  per-skill registry (agentsLead) — the single-skill assumption broke
  with a second skill.
- Category maps gain the harper-mcp categories; the ops key collision is
  avoided by naming the category `security`.
- Manifest: explicit mode: synthesized on every entry (house style);
  planned sources/must_cover mappings preserved as comments (the schema
  only permits them on generate rules) with a note that mcpPrompts is
  currently undocumented in reference/mcp/; cross_links added.
- package.json files + .npmignore include harper-mcp so the markdown
  ships to package consumers (dist/index.js export shape deliberately
  unchanged — flagged in the PR as a separate packaging decision).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kylebernhardy

Copy link
Copy Markdown
Member Author

Cross-model review round (Codex + Gemini) adjudicated in 0d8a451:

Codex (both findings taken):

  • harper-mcp is now in package.json files + .npmignore, so the markdown ships to package consumers. The dist/index.js export shape is deliberately unchanged — still flagged in the description as its own packaging decision.
  • The skill is registered in the generation pipeline's SKILLS registry, so validate-generated now actively lints its manifest and round-trips its AGENTS.md/SKILL.md index (both regenerated through the repo renderer so they pass by construction). This surfaced two latent single-skill assumptions, fixed here: the AGENTS.md lead paragraph is now per-skill (agentsLead in the registry), and the category maps gained the harper-mcp categories (the ops key collision avoided by naming the new category security).

Gemini (2 taken, 1 declined, 1 deferred-by-schema):

  • Explicit mode: synthesized added to every manifest entry (house style); cross_links added.
  • Sources mapped for automatic-verb-tools and custom-mcp-tools — but the manifest linter only permits live sources/must_cover on mode: generate rules, so all planned mappings are preserved as structured comments to uncomment with the flip.
  • Declined the Prefix-column suggestion: harper-best-practices' rule files don't carry prefixes either; the column is notational house convention.
  • Noted along the way: mcpPrompts has no documentation in reference/mcp/ (shipped 5.1.10) — recorded in the manifest as the reason that rule can't be docs-sourced yet; worth a docs issue.

Comment generated by an LLM (Claude Fable 5).

A clean-room agent evaluation (agent restricted to the skill as its
only Harper-MCP knowledge, building the protected docs-server scenario
against a live instance) surfaced four gaps, all fixed:

- Version-verification procedure: unsupported config keys — including
  the rateLimit.perClient*/quota.* security controls — are accepted
  and SILENTLY IGNORED by older Harper versions; the skill now says to
  check serverInfo.version and prove each protection denies once.
- The durable-quota example exposed its own counter table (exporting
  the hook class surfaces update_/delete_ verb tools and REST, letting
  clients reset their own counters); the example now carries
  exportTypes: { mcp: false } and a permissions note, and the
  security-posture checklist gained both items.
- Exported plain-Resource classes surface partial verb-tool families
  (a lone create_*) — now documented.
- "Tools stay in sync without restarts" gained its 5.1.18+ gate: the
  agent independently reproduced the pre-5.1.18 empty-registry-after-
  restart bug that harper#1613's lazy rebuild fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kylebernhardy

Copy link
Copy Markdown
Member Author

Ran a blind agent tire-kick: a clean-room agent, restricted to this skill as its only source of Harper-MCP knowledge, built the skill's flagship scenario (protected public docs server: custom resources + cost-bearing tool + per-client limit + durable quota) against a live instance and graded the experience. Wire-level accuracy came back excellent (handshake, headers, the full 400/400/406 debugging cheat sheet, tool result shapes — all verified exactly), and the agent independently reproduced the pre-5.1.18 restart-registration bug that harper#1613 fixed. Four real gaps surfaced and are fixed in the latest commit:

  1. Version verification is now a procedure, not a footnote — older versions accept and silently ignore the rateLimit.perClient*/quota.* security keys, so the skill now says: check serverInfo.version, then prove each protection denies once. (The evaluation ran on a pre-feature build, which made this failure mode vivid: config accepted, quota never invoked, no error anywhere.)
  2. The durable-quota example no longer exposes its own counter table — exporting the hook class surfaces update_/delete_ tools + REST for the quota table, letting clients reset their own counters. Example now carries exportTypes: { mcp: false }; the hardening checklist gained both this and the version-proof item.
  3. Documented that exported plain-Resource classes surface a partial verb family (a lone create_*).
  4. "Tools stay in sync without restarts" now carries its 5.1.18+ gate, with the pre-5.1.18 restart failure mode described.

Note for the docs repo: finding 2 applies to the merged quota example in reference/mcp/configuration.md as well — follow-up there.

Comment generated by an LLM (Claude Fable 5); evaluation by a sandboxed subagent.

@kriszyp

kriszyp commented Jul 9, 2026

Copy link
Copy Markdown
Member

Content checks out — cross-referenced every specific MCP claim (rate-limit config, quota hook contract, resource URIs, transport headers/status codes) against components/mcp/*.ts on main and it's accurate, including some genuinely subtle details.

Two things:

  1. The compiled harper-mcp/AGENTS.md ships with the wrong H1 (# Harper Best Practices, copy-pasted) — root cause is assembleAgentsMd in render.mjs hardcoding the title string instead of parameterizing it alongside agentsLead, which this PR does correctly thread through. Small fix: give each SKILLS manifest entry an agentsTitle and use it in place of the hardcoded string.
  2. Going forward we want to prefer generating skill content from documentation rather than hand-synthesizing — all 10 rules here are mode: synthesized even though rules.manifest.yaml already has commented-out sources:/must_cover: blocks mapping most of them 1:1 to existing pages under dev/documentation/reference/mcp/. That's real, actively-duplicated content today (913 lines covering much of the same ground). This mirrors how harper-best-practices itself started — fully synthesized, then flipped ~15 of 19 rules to mode: generate in follow-ups — so the staged approach isn't unreasonable, but given the stated preference, I'd rather see this PR (or an immediate follow-up) do the generate flip before/soon after merge rather than let two independently-maintained descriptions of the same surface drift apart.

— KrAIs

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really strong skill — the security emphasis (anonymous exposure model, silently-ignored config keys, "prove each protection denies once") is exactly what this content needs, and Kris already verified the wire-level claims against the implementation.

Requesting changes to get the outstanding feedback resolved before merge:

  1. Kris's H1 fix — the compiled harper-mcp/AGENTS.md ships titled # Harper Best Practices (hardcoded in assembleAgentsMd). Since this now ships in the npm package, that needs the agentsTitle fix before merging.
  2. Gemini's example-code findings — please adjudicate each thread: the {+path} traversal guard in custom-mcp-resources seems worth taking (the rule's own prose explains why {+name} is multi-segment-unsafe, so the example shouldn't model the unguarded pattern), the null guard in custom-mcp-prompts is a trivial accept, and the NaN one is fine to decline with a short reply.

Happy to re-review once those are in — the content itself is in great shape. Thanks!

sent with Claude Fable 5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants