@uvaresearch/wrapper-mcp
v0.1.6
Published
Stdio MCP server for QMS Wrapper task management (tools, resources, prompts).
Maintainers
Readme
@uvaresearch/wrapper-mcp
Stdio MCP server for QMS Wrapper. Lets MCP-capable clients (Claude Code, Claude Desktop, Cursor, generic stdio hosts) drive a QMS Wrapper instance over HTTPS using a Personal Access Token (PAT). Communication is JSON-RPC over stdin/stdout; outbound traffic is HTTPS to a single configured origin.
The server is built domain-by-domain. v0.1 ships task/form management — 8 task/form tools plus 2 project/organization lookup tools, 4 lookup / identity resources, 3 task/form resource templates, 5 prompts. A task and a form are the same underlying entity (forms are the custom tracker), so every task-form.* tool handles both. Subsequent versions will expand to additional QMS Wrapper domains (projects, processes, storage, risk, etc.) under the same package and PAT-auth model.
Install
Via npx (no install)
Use this in your MCP host configuration (see Step 2 below). npx will download the package on first run and cache it; subsequent runs reuse the cache.
npx -y @uvaresearch/wrapper-mcpVia npm install -g
For users who want wrapper-mcp on PATH:
npm install -g @uvaresearch/wrapper-mcp
wrapper-mcp --helpThe binary lands in your global npm bin dir. Find it with:
npm bin -gOn macOS/Linux this is typically /usr/local/bin or ~/.npm-global/bin; on Windows it is %AppData%\npm.
Configure
Step 1: get a Personal Access Token
- Log in to your QMS Wrapper instance.
- Go to Profile -> Access Tokens -> Create token.
- Name it for the device it will live on (e.g. "Claude Desktop laptop").
- Pick scopes. Defaults cover read/write/create on tasks. Tick
attachments:uploadif you need file uploads, plus whichever extra scope your wrapper instance requires for reassignment. - Submit and copy the
wrapper_<32 chars>plaintext that is shown exactly once. Store it in your OS keychain or a password manager.
Step 2: configure your MCP host
WRAPPER_BASE_URL is the URL you use to sign in to QMS Wrapper in the browser (the user-facing app). For the hosted SaaS that is https://app.qmswrapper.com. Self-hosted instances use their own host.
Claude Code
Either project-scoped (.mcp.json in repo root) or user-scoped (~/.claude.json under the mcpServers key):
{
"mcpServers": {
"wrapper": {
"command": "npx",
"args": ["-y", "@uvaresearch/wrapper-mcp"],
"env": {
"WRAPPER_PAT": "wrapper_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"WRAPPER_BASE_URL": "https://app.qmswrapper.com"
}
}
}
}Claude Desktop
Edit claude_desktop_config.json (Settings -> Developer -> Edit Config):
{
"mcpServers": {
"wrapper": {
"command": "npx",
"args": ["-y", "@uvaresearch/wrapper-mcp"],
"env": {
"WRAPPER_PAT": "wrapper_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"WRAPPER_BASE_URL": "https://app.qmswrapper.com"
}
}
}
}Restart Claude Desktop after editing.
Cursor
Edit ~/.cursor/mcp.json:
{
"mcpServers": {
"wrapper": {
"command": "npx",
"args": ["-y", "@uvaresearch/wrapper-mcp"],
"env": {
"WRAPPER_PAT": "wrapper_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"WRAPPER_BASE_URL": "https://app.qmswrapper.com"
}
}
}
}Generic stdio MCP client
Spawn wrapper-mcp (or npx -y @uvaresearch/wrapper-mcp) as a subprocess with WRAPPER_PAT and WRAPPER_BASE_URL in the child environment, and speak JSON-RPC over its stdin/stdout. Other hosts (for example GitHub Copilot CLI) generally follow the same shape, but config file locations differ; see https://modelcontextprotocol.io/clients for the current list.
Step 3: verify
In Claude Code, run /mcp and confirm:
- Server
wrappershows statusconnected. - Tools: 8. Resources: 4 (plus 3 templates). Prompts: 5.
CLI smoke test (no PAT required):
npx -y @uvaresearch/wrapper-mcp --help
npx -y @uvaresearch/wrapper-mcp --versionIf the server fails to start it writes a single-line reason to stderr and exits with code 64.
Environment variables
| Variable | Required | Default | Notes |
| ------------------- | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------ |
| WRAPPER_PAT | yes | | Personal access token, wrapper_<32 alnum>. Must come from env, not CLI flag. |
| WRAPPER_BASE_URL | yes | | Browser sign-in URL of your QMS Wrapper instance. HTTPS only; plain HTTP allowed only for loopback or *.local. |
| WRAPPER_UPLOAD_DIR| no | | Absolute path. Optional hardening for task-form.attach / storage.file.upload: when set, uploads must resolve inside this directory. When unset, uploads are allowed (still subject to absolute-path, symlink and size checks). |
| WRAPPER_UPLOAD_MAX_BYTES | no | 52428800 (50 MB) | Per-file upload size cap. |
| WRAPPER_CACHE_DIR | no | ~/.cache/wrapper-mcp | Scratch dir, created with mode 0700. |
| DEBUG | no | unset | When set (any value), emits verbose HTTP and session logs to stderr. |
Tools, resources, prompts at a glance
All v0.1 surfaces live in the task-form.* namespace. A task and a form are the same entity (forms are the custom tracker), so every tool covers both. Future domains will use sibling namespaces (project.*, process.*, etc.) without breaking existing tool names.
Task/form tools (8)
| Name | Effect | Annotations |
| -------------- | ------------------------------------------------ | -------------------------------------- |
| task-form.list | List my tasks/forms, newest first. | read-only, idempotent |
| task-form.get | Full task/form detail (journal, attachments, etc.). | read-only, idempotent |
| task-form.update | Batched change incl. reassign (R1, R2 enforced). | write, non-destructive |
| task-form.create | Create task/bug/form; requires previewed hash (R9). | write, non-destructive |
| task-form.relate | Link this task/form to another. | write, non-destructive |
| task-form.tag | Add/remove tags by name (idempotent). | write, idempotent |
| task-form.tags.list | List the org's existing tags (id + name). | read-only, idempotent |
| task-form.attach | Upload one or more files in one multipart POST. | write, non-destructive |
Lookup tools (2)
| Name | Effect | Annotations |
| ------------------- | ------------------------------------------------ | -------------------------------------- |
| project.members | List a project's assignable users + groups. | read-only, idempotent |
| organization.users | Search/list users across the organization. | read-only, idempotent |
Resources (4 concrete + 3 templates)
| URI | Returns |
| -------------------------------------------------- | ------------------------------------------------ |
| wrapper://lookups/statuses | All task/form statuses. |
| wrapper://lookups/priorities | All task/form priorities. |
| wrapper://lookups/trackers | All trackers (bug, task, etc.). |
| wrapper://me | Identity of the PAT owner. |
| wrapper://task-form/{id} | Full task/form detail. |
| wrapper://task-form/{id}/journal | Journal entries for the task/form. |
| wrapper://task-form/{id}/attachments/{fileId} | Signed download URL + metadata for one file. |
Task/form prompts (5)
| Slash command | Purpose |
| --------------- | ------------------------------------------------------------- |
| /qms-plate | Summarize my open tasks grouped by priority. |
| /qms-summary | Read a task plus attachments; produce a user-language recap. |
| /qms-start | Flip a task to in-progress in one atomic update. |
| /qms-finish | Close out a task with status=resolved + comment in one call. |
| /qms-file | Gather, preview, approve, hash, and create a new task (R9). |
Security and threat model
This section reflects what the code in src/ actually enforces. If a claim is not backed by code, it is not made here.
What the client does
- Opens one outbound TCP connection per request to
WRAPPER_BASE_URLusing Node's built-infetch. - Sends
Authorization: Bearer <PAT>plus a small set ofX-Mcp-*headers (X-Mcp-Session-Id,X-Mcp-Tool-Name, optionalX-Mcp-Request-Id) so the backend can audit each call. - Reads/writes only inside
WRAPPER_CACHE_DIR(default~/.cache/wrapper-mcp, created with mode 0700). The HTTP layer is stateless; the cache dir is reserved for ancillary scratch state. - For
task-form.attach, reads file paths supplied by the user in the tool arguments and includes their full contents as multipart fields. WhenWRAPPER_UPLOAD_DIRis set, reads are restricted to that directory; symlinks and oversized files are refused regardless.
What the client does NOT do
- No telemetry. No analytics. No usage pings.
- No auto-update. The binary you installed is the code that runs.
- No remote config. All configuration is local env vars and tool args.
- No outbound network calls beyond
WRAPPER_BASE_URL. There is no fallback host and no DNS prefetch. - No filesystem access outside
WRAPPER_CACHE_DIRand the explicit file paths passed totask-form.attach/storage.file.upload(confined toWRAPPER_UPLOAD_DIRwhen that is set).
What the PAT grants and does not grant
A PAT carries the scopes the user ticked at creation time. With default scopes the bearer can read, write, and create tasks they would normally have access to in the web UI. It is bearer-only: anyone who holds the plaintext value can act as the user until the token is revoked. It does not grant filesystem, OS, or shell access on the QMS Wrapper host. The server scopes the bearer to the issuing user; it does not elevate.
Hardening baked into v0.1.0
- URL guard (
src/security/urlGuard.ts): refusesWRAPPER_BASE_URLthat is nothttps://, except for loopback hosts (localhost,127.0.0.1,::1) and*.local. Also rejects userinfo (user:pass@host) and any RFC1918 / link-local / cloud-metadata IP literal. Fails closed at process start before any HTTP call. - Response scrubber (
src/security/responseScrubber.ts): runs over every tool / resource / error payload before it leaves the process and replaces anywrapper_<32 alnum>substring with[REDACTED]. The check happens on the serialized JSON string, so exotic types cannot bypass. If a match ever fires it also emits a stderr warning so the regression is visible. - Safe logger (
src/security/safeLog.ts): writes only to stderr (stdout is reserved for JSON-RPC framing). Every line is scrubbed twice: once by the PAT regex and once by literal-replacing the currentWRAPPER_PATenv value even if it does not match the regex shape. - CLI flag refused:
--pat,--token,--bearer,--auth,-p,-t, their=valueforms, glued shorts (-pSECRET), case variants, and positionalwrapper_<32 alnum>arguments all exit with code 64 before doing anything else. Rationale: command-line tokens land in shell history andps. - Minimal dependency tree: runtime deps are
@modelcontextprotocol/sdkandzod. No HTTP client library, no logger framework, no arg parser. Smaller supply-chain attack surface. - No telemetry, no auto-update, no remote config (see above).
task-form.attachvalidates every upload path: requires an absolute path, rejects symlinks, refuses non-regular files, and caps file size at 50 MB by default. SettingWRAPPER_UPLOAD_DIRadditionally confines uploads to that directory (recommended hardening; not required).
Known limitations
- Cookie jar deferred: the client is Bearer-only. If the backend ever starts requiring a session cookie alongside the PAT, this client will need a cookie store; today it sends no cookies.
task-form.attachreads each uploaded file fully into memory before posting. Very large attachments will be bounded by Node's heap.
What users should do
- Issue PATs with the narrowest scope set that gets your work done.
- Rotate PATs periodically; revoke on device loss or job change.
- Prefer HTTPS endpoints. The URL guard will refuse anything else outside of loopback /
*.localdev hosts. - Keep the PAT in the MCP host's
envblock (which most hosts read from a permissioned config file), not in shell rc files. - Set
WRAPPER_UPLOAD_DIRto a tightly-scoped directory to confinetask-form.attach/storage.file.uploadreads (optional, but recommended).
Troubleshooting
| Symptom | Likely cause / fix |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| WRAPPER_PAT is not set | Env var missing or empty in the MCP host config. Restart the host after editing the config file. |
| HTTP 401 from any tool | PAT expired or revoked. Mint a new one in Profile -> Access Tokens. |
| HTTP 403 with insufficient_scope | PAT lacks a scope. For task-form.attach tick attachments:upload; for other tools, tick the matching scope on the PAT. |
| HTTP 404 on every request | WRAPPER_BASE_URL points at the wrong host. Use the URL you would type into the browser to sign in (e.g. https://app.qmswrapper.com). |
| WRAPPER_BASE_URL must use https:// | URL guard rejected a plaintext HTTP URL on a non-loopback host. Use https:// or move to a localhost / *.local dev URL.|
| PAT must be supplied via the WRAPPER_PAT environment variable | You passed --pat=... on the command line. Move the token into the env block of your MCP host config. |
| upload refused: file path ... resolves outside WRAPPER_UPLOAD_DIR | WRAPPER_UPLOAD_DIR is set and the file lives outside it. Point it at a parent of the files, or unset it to allow any absolute path. |
| Empty tools list in Claude Code after /mcp | The server crashed at startup. Run wrapper-mcp --version in a terminal to confirm the binary works, then check stderr. |
| npm install fails | Node 20+ is required. Check node --version. |
| previewedHash mismatch | You called task-form.create directly. Use the /qms-file prompt; it gathers fields, previews, hashes, and posts in one flow. |
| task-form.update refused: assigneeId requires userExplicitlyRequested: true (R1) | The agent tried to reassign without explicit user instruction. Confirm with the user, then re-call with the flag set on task-form.update. |
License
MIT. See LICENSE.
