Overview
Build a minimal MCP server for the Hypothesis web annotation API (https://web.hypothes.is/).
A stdio MCP server in TypeScript that lets Claude Code agents read and write Hypothesis annotations, focused on reading group annotations to drive code/doc updates.
Tech stack
Section titled “Tech stack”- TypeScript (strict)
- @modelcontextprotocol/sdk (stdio server)
- Native fetch (Node 18+)
- Vitest for unit tests
- ESLint with typescript-eslint (strictTypeChecked)
- eslint-plugin-unicorn (selective rules)
- Prettier for formatting
- lint-staged + simple-git-hooks for commit hygiene
- No frameworks — stdio only
Read HYPOTHESIS_API_TOKEN from 1Password (MCP configuration should use 1Password CLI) with its reference: op://Private/Hypothesis-API/credential. Base URL is https://hypothes.is/api.
Tools to implement
Section titled “Tools to implement”-
get_groups — GET /api/profile/groups Returns user’s group memberships (id, name, public id).
-
search_annotations — GET /api/search Params: group? (pubid), uri?, text?, user?, tag?, limit? (default 50, max 200), offset? Returns: array of annotations with id, uri, text, tags, user, created, updated, target (includes quote/selector).
-
get_annotation — GET /api/annotations/:id Returns full annotation detail.
-
get_group_annotations — POST /api/bulk/annotation Fetches all annotations for a group efficiently. Params: group (pubid), limit? (default 200). Use the bulk endpoint for large datasets.
-
create_annotation — POST /api/annotations Params: uri (required), text (required), group? (pubid, defaults to “world” i.e. public), tags? (string[]) Minimal payload — no need to handle target selectors for now.
-
update_annotation — PATCH /api/annotations/:id Params: id (required), text?, tags?
Annotation shape to expose (normalized, drop noise)
Section titled “Annotation shape to expose (normalized, drop noise)”interface Annotation { id: string uri: string // page URL annotated text: string // annotation body tags: string[] user: string // acct:username@hypothes.is group: string // group pubid created: string // ISO updated: string // ISO quote?: string // selected text (from target[0].selector, TextQuoteSelector)}TypeScript config
Section titled “TypeScript config”Strict mode plus these additional flags:
{ "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true}target ES2022, module NodeNext, outDir dist.
ESLint config
Section titled “ESLint config”- Use
typescript-eslintflat config withstrictTypeCheckedruleset parserOptions.projectpointing at tsconfig.json- Add
eslint-plugin-unicornwith these rules enabled selectively:unicorn/prefer-node-protocol(enforcenode:imports)unicorn/no-nested-ternaryunicorn/prefer-moduleunicorn/throw-new-error
- Run ESLint and Prettier as separate concerns (do NOT use eslint-plugin-prettier)
Prettier config
Section titled “Prettier config”{ "singleQuote": true, "semi": false, "trailingComma": "all"}Git hooks (simple-git-hooks + lint-staged)
Section titled “Git hooks (simple-git-hooks + lint-staged)”On pre-commit: run prettier --write then eslint --fix on staged .ts files only.
"simple-git-hooks": { "pre-commit": "npx lint-staged"},"lint-staged": { "*.ts": ["prettier --write", "eslint --fix"]}Run npx simple-git-hooks after install to register the hook.
Error handling
Section titled “Error handling”- Throw descriptive errors on non-2xx responses, include status + body
- Validate required params and throw before making the request
- No unhandled promise rejections — ESLint’s strictTypeChecked will enforce this
Project structure
Section titled “Project structure”tools/mcp/hypothesis/ src/ index.ts # MCP server setup and tool registration client.ts # Hypothesis API client (typed fetch wrapper) types.ts # Shared types tests/ client.test.ts # Vitest unit tests for API client tools.test.ts # Vitest unit tests for tool input validation and response mapping package.json tsconfig.json .eslintrc.js (or eslint.config.js for flat config) .prettierrc vitest.config.ts README.mdpackage.json requirements
Section titled “package.json requirements”"type": "module"- Scripts:
build:tscstart:node dist/index.jstest:vitest runtest:watch:vitestlint:eslint src testsformat:prettier --write src teststypecheck:tsc --noEmit
- bin entry so it can be run directly after build
Tests to write (Vitest)
Section titled “Tests to write (Vitest)”- Mock
fetchusingvi.stubGlobal— do not make real HTTP calls client.test.ts:- Each API method constructs the correct URL and headers
- Non-2xx responses throw with status and body included in message
quoteis correctly extracted fromTextQuoteSelectorin target selectors- Missing required params throw before fetch is called
tools.test.ts:search_annotationsdefault limit is applied when omittedget_group_annotationsuses the bulk endpointcreate_annotationdefaults group to__world__when omitted
Constraints
Section titled “Constraints”- No class-based architecture — plain functions only
- Keep src under 300 lines total
- No express or any HTTP server — stdio only
- All
node:built-in imports must use thenode:protocol prefix - No
anytypes — ESLint strictTypeChecked will flag them
README should include
Section titled “README should include”- Install, build, and test steps
- How to get a Hypothesis API token (https://hypothes.is/account/developer)
- Claude Code MCP config snippet:
{ "mcpServers": { "hypothesis": { "type": "stdio", "command": "node", "args": ["/absolute/path/to/hypothesis-mcp/dist/index.js"], "env": { "HYPOTHESIS_API_TOKEN": "$(op read 'op://Private/Hypothesis-API/credential')" } } }}Deliverable
Section titled “Deliverable”Working, buildable, tested code. After writing all files:
- Run
npm install - Run
npx simple-git-hooksto register git hooks - Run
npm run build— must compile with zero errors - Run
npm run lint— must pass with zero errors - Run
npm test— all tests must pass
Fix any type, lint, or test errors before finishing.
Copyright: © Arda Systems 2025-2026, All rights reserved