MCP (Model Context Protocol) server for Hudu IT documentation platform. Provides 29 tools and 6 resources for managing companies, assets, articles, passwords, and more through any MCP-compatible client.
- 29 MCP tools covering the major Hudu resources (no delete tools — see below)
- 6 MCP resources (3 list resources + 3 by-ID resource templates) for direct data access
- Dual transport support: stdio (default) and HTTP Streamable
- Vendored Hudu API client (
src/vendor/hudu) — no private registry / token to build - Read-safe secrets — password tools and password/OTP-type asset fields never return secret values
- No destructive deletes — delete tools are intentionally not exposed
- Company access control via
HUDU_DISALLOWED_COMPANY_IDS - Transient-DNS retry on Hudu API calls (handles container
EAI_AGAINblips) - Lazy initialization - SDK client created on first tool call
- Connection testing built-in
- All logging to stderr to avoid polluting MCP stdio transport
Note
The Hudu API client is vendored in-repo (src/vendor/hudu), so builds need no
registry token — every dependency is public on npmjs. Just deploy; the cloud
builder runs npm install with no extra credentials.
Note
The DigitalOcean target builds the full Docker image and runs the complete MCP
server over HTTP — this is the recommended path for operators. This repo does not
ship a wrangler.json/Workers entrypoint, so for a self-hosted server prefer
DigitalOcean or the prebuilt container image (ghcr.io/wyre-technology/hudu-mcp).
All dependencies are public on npmjs (the Hudu API client is vendored under
src/vendor/hudu), so no registry auth or token is required:
git clone https://github.com/wyre-technology/hudu-mcp.git
cd hudu-mcp
npm install
npm run build| Variable | Required | Default | Description |
|---|---|---|---|
HUDU_BASE_URL |
Yes | - | Your Hudu instance URL (e.g., https://docs.example.com) |
HUDU_API_KEY |
Yes | - | Your Hudu API key |
HUDU_DISALLOWED_COMPANY_IDS |
No | - | Comma-separated company IDs to hide from all tools and resources (companies + their assets, articles, passwords). See Restricting companies. |
MCP_TRANSPORT |
No | stdio |
Transport type: stdio or http |
MCP_HTTP_PORT |
No | 8080 |
HTTP server port (when using http transport) |
MCP_HTTP_HOST |
No | 0.0.0.0 |
HTTP server host |
MCP_SERVER_NAME |
No | hudu-mcp |
Server name reported to MCP clients |
MCP_SERVER_VERSION |
No | 1.0.0 |
Server version reported to MCP clients |
LOG_LEVEL |
No | info |
Log level: error, warn, info, debug |
LOG_FORMAT |
No | simple |
Log format: json or simple |
Set HUDU_DISALLOWED_COMPANY_IDS to a comma-separated list of company IDs to make the
server behave as if those companies — and everything filed under them — do not exist:
HUDU_DISALLOWED_COMPANY_IDS=123,456The filter is enforced for both tools and resources:
hudu_list_companies(and thehudu://companiesresource) omit the listed IDs.hudu_get_companyand the mutating company tools reject a disallowed ID with an error.- List endpoints for assets, articles, passwords, folders, procedures, relations, and
Magic Dash drop rows whose
company_idis disallowed — this is the primary guard, since it's what hides those records (and their IDs) from a client. - Getting a child record whose
company_idis disallowed, or creating/updating one with a disallowedcompany_idin the payload, is rejected. Records with no company (global articles, asset layouts) are unaffected.
Note:
hudu_archive_articleby raw ID is not pre-checked against the disallow list (an article isn't fetched first to resolve its company). Asset archive is checked, since it resolves the owning company first. In practice a client can't reach a disallowed company's record IDs anyway — list and get already hide them.
The repo ships a docker-compose.yml that builds the image and runs the server as an
HTTP service on port 3100 — no local Node/npm and no registry token required (the Hudu
SDK is vendored in-repo, and Docker runs the install during the build).
cp .env.example .env # then edit: HUDU_BASE_URL, HUDU_API_KEY, HUDU_DISALLOWED_COMPANY_IDS
docker compose build
docker compose up -d
curl http://localhost:3100/health # verify it's runningDocker Compose auto-loads .env for variable substitution. Point your reverse proxy /
MCP client at the /mcp endpoint on port 3100.
Add to your claude_desktop_config.json:
{
"mcpServers": {
"hudu": {
"command": "node",
"args": ["/path/to/hudu-mcp/dist/entry.js"],
"env": {
"HUDU_BASE_URL": "https://docs.example.com",
"HUDU_API_KEY": "your-api-key"
}
}
}
}HUDU_BASE_URL=https://docs.example.com \
HUDU_API_KEY=your-api-key \
MCP_TRANSPORT=http \
MCP_HTTP_PORT=8080 \
npm startThe HTTP server exposes two endpoints: POST /mcp (the MCP Streamable HTTP endpoint —
stateless JSON, no session affinity) and GET /health (liveness). Front it with a reverse
proxy for TLS/auth and point your MCP client at /mcp.
No delete tools. Deleting companies, assets, passwords, and articles is intentionally not exposed by this server. The most destructive available action is archive (reversible).
| Tool | Description |
|---|---|
hudu_list_companies |
List companies with optional filters |
hudu_get_company |
Get a company by ID |
hudu_create_company |
Create a new company |
hudu_update_company |
Update an existing company |
hudu_archive_company |
Archive a company |
hudu_unarchive_company |
Unarchive a company |
hudu_test_connection |
Test the connection to Hudu API |
| Tool | Description |
|---|---|
hudu_list_assets |
List/search assets — name keyword-matches name, identity fields & custom fields |
hudu_get_asset |
Get an asset by ID |
hudu_create_asset |
Create a new asset (requires company_id) |
hudu_update_asset |
Update an existing asset (company_id optional — auto-resolved) |
hudu_archive_asset |
Archive an asset (company_id optional — auto-resolved) |
Hudu addresses a single asset under its owning company (
/api/v1/companies/{company_id}/assets/{id}). Forupdate/archiveyou may passcompany_idexplicitly, but if you omit it the server resolves it from the asset automatically — so{ "id": 333 }is enough.Keyword search. Hudu's own
namefilter is exact-match, sohudu_list_assetsandhudu_list_articlesimplementnameas a case-insensitive keyword search instead: whennameis set the server fetches the (server-filtered) set across all pages and matches locally. Assets match on name, identity fields (serial/model/manufacturer) and custom field labels/values; articles match on name, body content, and the article's full folder path (e.g. "Cloudflare" finds an article in the "Cloudflare setup" folder). Scope withcompany_idto keep these searches fast.
| Tool | Description |
|---|---|
hudu_list_asset_layouts |
List asset layouts |
hudu_get_asset_layout |
Get an asset layout by ID |
hudu_create_asset_layout |
Create a new asset layout |
hudu_update_asset_layout |
Update an existing asset layout |
| Tool | Description |
|---|---|
hudu_list_asset_passwords |
List asset passwords (secret/OTP values omitted) |
hudu_get_asset_password |
Get an asset password by ID (secret/OTP values omitted) |
hudu_create_asset_password |
Create a new asset password |
hudu_update_asset_password |
Update an existing asset password |
Secrets are never returned. The read/create/update password tools strip the
passwordandotp_secretfields before responding — callers see name, username, url, type, and notes, but never the secret value or OTP seed. (Create/update still accept a password to store; they just don't echo it back.) Likewise, password/OTP-type custom fields on assets have their values replaced with[redacted]in every asset response (hudu_get_asset,hudu_list_assets, create/update), and such values are not searchable.
| Tool | Description |
|---|---|
hudu_list_articles |
List/search articles — name keyword-matches name, body & folder path |
hudu_get_article |
Get an article by ID |
hudu_create_article |
Create a new article |
hudu_update_article |
Update an existing article |
hudu_archive_article |
Archive an article |
| Tool | Description |
|---|---|
hudu_list_folders |
List folders |
hudu_list_procedures |
List procedures |
hudu_list_relations |
List relations |
hudu_list_magic_dash |
List Magic Dash items |
| Tool | Description |
|---|---|
hudu_add_attachment |
Attach a file to a record via POST /api/v1/uploads. Required: uploadable_type (PascalCase — Asset, Article, …), uploadable_id, filename, and exactly one of file_url / content. |
hudu_list_uploads |
List stored files as {id, name, mime, size, …}. Bytes not included. |
hudu_get_upload |
Download one file and return it as MCP content blocks. |
hudu_get_public_photo |
View an image embedded inside an article body, as an image block. Takes the numeric_id from an article's public_photos array (a slug from the body's <img src> also works, via a lookup). |
Uploads and public photos are separate namespaces. A file attached to a record is an
upload; an image inside an article body is a public photo. Their ids do not overlap, so
hudu_list_uploads never shows an inline image and hudu_get_upload cannot fetch one — hence
the two retrieval tools.
Images inside an article body are not attachments. hudu_create_article and
hudu_update_article take an images array plus {{image:N}} placeholders in content; each
image is uploaded via POST /api/v1/public_photos and the placeholder is rewritten to an
<img> tag pointing at the hosted URL. That is Hudu's own mechanism, and it keeps the article
record small — a data URI would bloat it permanently and be re-read on every fetch.
Creating an article with images is a two-pass operation, unavoidably: public_photos needs a
record_id, which does not exist until the article does. The tool POSTs the article, uploads
the photos against the new id, then PUTs the spliced content. If a photo fails, the article is
still written and the response names the placeholders left unfilled rather than silently
shipping a body with {{image:2}} in it.
This server implements the MCP attachment standard,
with one deliberate divergence: no image downscaling. hudu-mcp also builds for Cloudflare
Workers, where sharp (a native module) cannot run, so an image over IMAGE_MAX_BYTES is
refused with a note rather than shrunk to fit. Both write paths are unaffected.
List resources (returned by resources/list):
| URI | Description |
|---|---|
hudu://companies |
List of all companies |
hudu://assets |
List of all assets |
hudu://articles |
List of all articles |
By-ID resource templates (returned by resources/templates/list):
| URI Template | Description |
|---|---|
hudu://companies/{id} |
Company details by ID |
hudu://assets/{id} |
Asset details by ID |
hudu://articles/{id} |
Article details by ID |
The Hudu API client lives in-repo at src/vendor/hudu (forked from @wyre-technology/node-hudu)
rather than being pulled from a private registry. This removes the GitHub Packages dependency and
the build-time token entirely, and lets us correct Hudu's routing where the upstream client was
wrong. Notably, single-asset operations are nested under the company
(/api/v1/companies/{company_id}/assets/{id}[/archive]); only the asset list endpoint is
top-level (and supports an id filter, which is how get_asset resolves a single asset without a
company). No-body mutations (archive/unarchive/delete) are sent without a Content-Type: application/json
header, since Hudu's backend 500s trying to parse an empty JSON body. Asset custom_fields are
normalized to Hudu's required array-of-objects shape (a single { } object is wrapped into [ { } ]).
Hudu API calls are wrapped with a short retry that fires only on pre-connection failures
(EAI_AGAIN / ENOTFOUND / ECONNREFUSED) — common as intermittent DNS blips inside containers.
These are safe to retry even for writes because the request never reached Hudu. Other errors
(4xx/5xx, timeouts mid-flight) are not retried.
# Install dependencies
npm install
# Build
npm run build
# Run in development mode
npm run dev
# Clean build output
npm run clean