Skip to content

Development

JoKeks2023 edited this page Sep 28, 2026 · 1 revision

Development

Setup

git clone https://github.com/JoKeks2023/huly-mcp.git
cd huly-mcp
npm install
cp .env.example .env     # fill in credentials (only needed for live runs and scripts)

npm scripts

Script Command Purpose
npm run build tsc Compile src/ → dist/
npm test node --import tsx --test test/*.test.ts Run the test suite (no Huly server needed)
npm run dev tsx watch src/index.ts Run from source with reload
npm start node dist/index.js Run the built server (stdio)
npm run setup node scripts/setup.js Token wizard + workspace discovery test (Setup wizard)

The server itself does not load .env. It reads the process environment, which the MCP client, docker --env-file or your shell provide. scripts/demo.js and scripts/cleanup-issues.js load .env themselves.

Project layout

src/
  index.ts            stdio entry point, shutdown handling
  server.ts           tool registration + server instructions
  schemas.ts          zod schemas (WorkspaceArg / ReadWorkspaceArg)
  connection.ts       WorkspaceConnectionManager (auth, discovery, per-workspace connections)
  tools/*.ts          tool handlers, one file per area (workspaces.ts, issues.ts, …)
  utils/              errors (wrapToolHandler), format, markdown → ProseMirror, storage (/files, datalake)
test/
  helpers.ts          in-memory fakes: account service, per-workspace TxOperations
  connection.test.ts  auth, discovery, caching, isolation, legacy filter, shutdown
  tools.test.ts       tool routing, mutations, same IDs across workspaces, schema rules, token regression tests
scripts/              setup wizard and helper scripts

Tests

Tests use Node's built-in node:test runner with tsx. There are no extra test dependencies, and no Huly server or network access is needed.

  • test/helpers.ts → createFakeEnv({ workspaces, data, token, tokenInfo, restrictTo, … }) builds a real WorkspaceConnectionManager with injected fakes:
    • an account client that records every login/getUserWorkspaces/selectWorkspace call and the token it was called with
    • an openConnection that returns an in-memory FakeTxClient per workspace, recording all mutations
  • Tool tests install the fake manager with setConnectionManager() and call the real tool handlers.
  • globalThis.fetch is mocked for /files tests.

Invariants enforced by tests (keep them green when changing things):

  • every mutation schema requires workspace, and every registered tool except list_workspaces accepts it
  • no "last used" fallback; blank workspace is rejected
  • one connection per workspace UUID, concurrent first use opens one connection
  • getUserWorkspaces/selectWorkspace use the account token, getWorkspaceMembers the workspace token
  • no /files request URL and no tool result contains a token

Typecheck the tests (they are not part of tsc's src/ build):

npx tsc --noEmit --strict --esModuleInterop --skipLibCheck --module node16 --moduleResolution node16 --target es2022 --lib es2022,dom test/*.ts

There is no linter configured in the repository.

Adding a tool

  1. Schema in src/schemas.ts, with the correct workspace field:
    export const ArchiveProjectSchema = z.object({
      workspace: WorkspaceArg,          // changes data → required
      identifier: z.string().describe('Project identifier, e.g. "PROJ"')
    })
    Use ReadWorkspaceArg only for tools that never change anything.
  2. Handler in src/tools/<area>.ts, resolving its connection from its own argument:
    export const archiveProject = wrapToolHandler<z.infer<typeof ArchiveProjectSchema>>(async (args) => {
      const client = await getConnection(args.workspace)
      // … use getWorkspaceInfo(args.workspace) for /files; send the token only as a Bearer header
      return '✅ …'
    })
    Never cache a workspace or client in module state.
  3. Register in src/server.ts with server.tool(name, description, Schema.shape, handler).
  4. Tests: a writing tool must be added to MUTATION_SCHEMAS in test/tools.test.ts. The registration test identifies writing tools by name prefix (create_, update_, delete_, add_, remove_, set_, log_, link_, send_, attach_, start_), so stick to these prefixes or extend the list.
  5. Update the tool tables in the README and in Tools.

Helper scripts

All three need the compiled dist/ (npm run build) and credentials in the environment. Because they write data, they need an explicit workspace: --workspace=<slug|name|id>, falling back to HULY_WORKSPACE. Without either they stop with "No workspace given."

Script Usage
scripts/import-csv.js node scripts/import-csv.js tasks.csv PROJ --workspace=acmeteam. Imports issues from CSV (title required; priority, status, dueDate optional)
scripts/cleanup-issues.js node scripts/cleanup-issues.js PROJ 59 68 --workspace=acmeteam [--confirm]. Deletes an issue range; dry run without --confirm
scripts/demo.js node scripts/demo.js --workspace=acmeteam. Live demo: creates, updates and deletes a test issue

Releasing

  • Docker: automatic on push to main and v* tags (Docker Deployment).
  • npm: published manually (prepublishOnly runs the build). npm-publish-ghp.yml mirrors releases to GitHub Packages as @jokeks2023/huly-mcp-selfhost.
  • The server.json in the repository is still upstream's MCP registry entry and does not describe this fork.

Conventions

  • No squash merges; merge commits or rebase merges only.
  • Behavior changes are called out explicitly in commit messages and PR descriptions.

Clone this wiki locally