Skip to content

Simple Hypothesis MCP

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.

  • 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.

  1. get_groups — GET /api/profile/groups Returns user’s group memberships (id, name, public id).

  2. 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).

  3. get_annotation — GET /api/annotations/:id Returns full annotation detail.

  4. 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.

  5. 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.

  6. 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)
}

Strict mode plus these additional flags:

{
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true
}

target ES2022, module NodeNext, outDir dist.

  • Use typescript-eslint flat config with strictTypeChecked ruleset
  • parserOptions.project pointing at tsconfig.json
  • Add eslint-plugin-unicorn with these rules enabled selectively:
    • unicorn/prefer-node-protocol (enforce node: imports)
    • unicorn/no-nested-ternary
    • unicorn/prefer-module
    • unicorn/throw-new-error
  • Run ESLint and Prettier as separate concerns (do NOT use eslint-plugin-prettier)
{
"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.

package.json
"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.

  • 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
Terminal window
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.md
  • "type": "module"
  • Scripts:
    • build: tsc
    • start: node dist/index.js
    • test: vitest run
    • test:watch: vitest
    • lint: eslint src tests
    • format: prettier --write src tests
    • typecheck: tsc --noEmit
  • bin entry so it can be run directly after build
  • Mock fetch using vi.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
    • quote is correctly extracted from TextQuoteSelector in target selectors
    • Missing required params throw before fetch is called
  • tools.test.ts:
    • search_annotations default limit is applied when omitted
    • get_group_annotations uses the bulk endpoint
    • create_annotation defaults group to __world__ when omitted
  • 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 the node: protocol prefix
  • No any types — ESLint strictTypeChecked will flag them
{
"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')"
}
}
}
}

Working, buildable, tested code. After writing all files:

  1. Run npm install
  2. Run npx simple-git-hooks to register git hooks
  3. Run npm run build — must compile with zero errors
  4. Run npm run lint — must pass with zero errors
  5. Run npm test — all tests must pass

Fix any type, lint, or test errors before finishing.