# DeepSeek Harness Plugin for Reactive Resume — Design **Date:** 2026-08-16 **Status:** Approved, ready for implementation planning **Package:** `dsh-plugin-reactive-resume` > **Post-implementation note:** the `tools` config key and `ctx.tools.restrict()` call described below (Config, `apply()` step 2, "Tool-name generation and drift detection") were cut from v0.1.0. `docs/spikes/2026-08-16-restrict-semantics.md` found that `restrict()` cannot reach tools a plugin's own `ctx.plugin(mcpClient, …)` registers — it throws when called from an unscoped plugin context, and even from a real agent scope it refuses to touch a scope's own (as opposed to inherited) registrations. There is no arrangement reachable from this plugin's `apply(ctx, config)` where curation works, so 0.1.0 ships all tools unfiltered, per that spike's documented fallback. The rest of this document is left as originally written for history; it does not describe what shipped. ## Goal Distribute a DeepSeek Harness plugin that connects Harness to a Reactive Resume account, so a DSH user can read, create, and edit resumes and job applications from their agent session with one config row and an API key. ## Context ### What Reactive Resume already ships Reactive Resume exposes a complete remote MCP server. Nothing on the server side needs to change for this plugin to work. - `packages/mcp` registers 33 tools (`list_resumes`, `read_resume`, `apply_resume_patch`, `tailor_resume_for_application`, …), 3 prompts, and 2 resources. - `apps/server/src/http/app.ts` mounts `/mcp` and `/mcp/*` (Streamable HTTP) plus `/.well-known/mcp/server-card.json` (SEP-1649). - `apps/server/src/mcp/auth.ts` accepts either an OAuth `Authorization: Bearer` token or an `x-api-key` header. The API key path needs no interactive flow. - The web app already ships API key management at `/dashboard/settings/api-keys`, so user provisioning is a solved problem. ### What DeepSeek Harness provides Verified against the published packages, not only the docs. - A plugin is a TypeScript module exporting `name`, optional `inject`, and `apply(ctx, config)`. Config schemas use `@deepseek-ai/schemastery`. - `@deepseek-ai/dsh-mcp-client` (v0.0.1-rc.1) bridges one external MCP server per plugin instance. Its `StreamableHttpConfig` is exactly: ```ts interface StreamableHttpConfig { transport: 'streamable-http' serverName: string // [A-Za-z0-9_-]{1,32}, unique across live instances url: string headers: Record toolCallTimeoutMs: number failOnStartupError: boolean } ``` Bridged tools become model-facing as `mcp____`. - `ctx.tools` is a `ToolRegistry` exposing `restrict(filter: ToolRestriction): () => void`, where `ToolRestriction` is `{ allow?: readonly string[]; deny?: readonly string[] }`. - `ctx.systemPrompt` (from `@deepseek-ai/dsh-system-prompt`) exposes `section(section: PromptSection): () => void`, where `PromptSection` is `{ name, order, text, complete? }`. Convention: `-100` is harness identity, `0` the deployment persona, and `100–199` is tool guidance. - Plugins are distributed on npm and discovered through the `dsh-plugin` GitHub topic. ### The gap this plugin fills `dsh-mcp-client` has no tool filtering and no way to contribute prompt text. A user pasting a raw MCP row into `cordis.yml` gets all 33 tool schemas in their context budget and no guidance on Reactive Resume's JSON Patch semantics. Those two things are the plugin's reason to exist. ## Decisions | Decision | Choice | Why | |---|---|---| | Scope | Thin MCP bridge + prompt section | The tool layer already exists and is maintained in Reactive Resume. Reimplementing 33 tool contracts against oRPC would drift every release. | | Repo | Standalone, outside the Reactive Resume monorepo | The plugin imports nothing from Reactive Resume — it speaks HTTP. The monorepo has no npm publish pipeline: root and every package are `private: true`, there is no build output, no changesets, no `NPM_TOKEN`, and no publish workflow. Adding one to ship a dependency-free package is cost without benefit. | | Auth | `x-api-key` only | Two clicks in Reactive Resume settings. OAuth needs a token store and an interactive flow for no gain. | | Drift protection | Server-card contract test in CI | Replaces the lockstep the monorepo would have given, without the pipeline. | ## Architecture ### Package shape Namespace plugin, mirroring `dsh-mcp-client`'s own export form: ```ts export const name = 'reactive-resume' export const inject = ['tools', 'systemPrompt'] export const Config: z export async function apply(ctx: Context, config: Config): Promise ``` `@deepseek-ai/cordis`, `@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-system-prompt`, and `@deepseek-ai/dsh-mcp-client` are `peerDependencies` so the plugin binds to the host's versions rather than installing a second copy of the runtime. ### Config ```ts interface Config { /** API key from /dashboard/settings/api-keys. */ apiKey: string /** Reactive Resume instance origin. Default 'https://rxresu.me'. */ url?: string /** Tool namespace: tools appear as mcp____. Default 'resume'. */ serverName?: string /** Which tool group to expose. Default 'all'. */ tools?: 'resume' | 'applications' | 'all' /** Per-tool-call timeout. Default inherited from dsh-mcp-client. */ toolCallTimeoutMs?: number } ``` `url` accepts any origin so self-hosted instances work unchanged. ### apply() Three steps, in order: 1. **Mount the bridge.** `ctx.plugin(mcpClient, { transport: 'streamable-http', serverName, url:`${url}/mcp`, headers: { 'x-api-key': apiKey }, toolCallTimeoutMs, failOnStartupError: true })`. `failOnStartupError: true` turns a bad API key into a loud activation failure instead of tools that silently fail at call time. 2. **Curate the tool surface.** When `tools !== 'all'`, call `ctx.tools.restrict({ deny: [...] })` with the namespaced names of the excluded group, computed from the generated tool-name list. See "Open risk" below. 3. **Contribute prompt guidance.** `ctx.systemPrompt.section({ name: 'reactive-resume', order: 150, text: PATCH_GUIDE })`. All three return disposers; Cordis effect scoping unwinds them on unload, so no manual cleanup is needed beyond returning them where the API expects it. ### PATCH_GUIDE The plugin's substance. Roughly 40 lines covering the failure modes Reactive Resume already encodes as error hints in `packages/mcp/src/tools.ts` (`errorHint`) — those hints exist precisely because models get these wrong: - Call `read_resume` before `apply_resume_patch`; never patch blind. - `apply_resume_patch` takes RFC-6902 operations against the resume data document. - Fetch the `resume://_meta/schema` resource before constructing paths. - Section entries are arrays of objects keyed by UUID; ids are not indices. - A locked resume rejects writes — call `unlock_resume` first. - `list_resumes` is the way to recover a valid id after a 404. Written as static text, not a provider function — it does not vary per assembly. ### Tool-name generation and drift detection > **Superseded by the move into the monorepo.** The plugin now sits beside the MCP server it bridges, so `src/tool-names.test.ts` reads `@reactive-resume/mcp/tool-names` directly. A tool rename fails that test on the same pull request. The generated snapshot, the fetch script, and the scheduled network job described below no longer exist. - Build step fetches `/.well-known/mcp/server-card.json` and emits `src/tool-names.generated.ts` containing the raw tool names split into the `resume` and `applications` groups. - The generated file is committed, so `npm install` never needs network access. - A scheduled CI job refetches from `https://rxresu.me` and fails when the committed list no longer matches the live card. That failure is the signal to cut a new plugin release. ### Install UX README's copy-paste block: ```yaml - insert: - id: reactive-resume name: dsh-plugin-reactive-resume config: apiKey: !!js process.env.RXRESUME_API_KEY ``` ## Open risk `ToolRegistry.restrict`'s contract reads: *"Per-scope filter over the tools a scope INHERITS — the global layer and every ancestor layer on its chain. Restrictions intersect, and do not affect the scope's own registrations."* `ctx.plugin(mcpClient, …)` mounts the bridge in a **child** scope of the plugin's context. The bridged tools are therefore registered in a descendant, not an ancestor, of the scope calling `restrict`. Whether a parent-scope restriction reaches them is unverified. This must be settled by a spike before the config surface is committed, because `tools` is a public config key and removing it later is a breaking change. **Fallback if `restrict` does not reach the child scope:** ship 0.1.0 without the `tools` key (equivalent to `'all'`), and solve curation in 0.2.0 — possibly by mounting the bridge at the same scope level rather than as a child. The prompt section alone justifies the release. ## Testing - **Spike (blocking):** run a local Reactive Resume (`dotenvx run -f .env.local -- pnpm dev`, port 3000), point a scratch Harness config at `http://localhost:3000/mcp` with an `x-api-key`, and confirm (a) the Streamable HTTP bridge connects and lists 33 tools, and (b) whether `ctx.tools.restrict` from the plugin's scope hides bridged tools. - **Unit:** `Config` schema defaults and validation; the deny-list computation from the generated tool names; `PATCH_GUIDE` section registers at order 150 with the expected name. - **Contract:** server-card fetch matches `src/tool-names.generated.ts`. - **Manual smoke before publish:** in a real Harness session against rxresu.me — list resumes, read one, apply a patch, verify the change in the web UI. ## Out of scope OAuth support, `ctx.commands` shortcuts, a bundled Harness skill, local resume caching, and PDF rendering inside Harness. Each is additive and none blocks a useful 0.1.0. ## Build order 0. Spike `restrict()` semantics and the Streamable HTTP bridge against localhost. 1. Repo scaffold, `Config` schema, bridge mount, README. End-to-end working install. 2. `PATCH_GUIDE` prompt section. 3. Tool profiles, generated name list, drift CI. 4. Publish 0.1.0, tag the repo with the `dsh-plugin` topic, submit to `awesome-deepseek-harness`.