weeek-mcp
v0.1.0-rc.1
Published
Local read-only MCP server for Weeek (stdio transport).
Maintainers
Readme
weeek-mcp
Локальный read-only MCP-сервер для Weeek, общается с AI-клиентами (Claude Desktop, Claude Code, Cursor, MCP Inspector) по stdio.
English README · docs/reviews/roadmap.md (полная мета-roadmap)
Статус: I-pub — публикация в npm (0.1.0-rc.1 под --tag next),
RU README, публичный ROADMAP.md. Закрывает захват namespace
weeek-mcp и DX-разрыв для русскоязычной аудитории. Ноль правок в
src/** за пределами одного строкового литерала версии. Поверхность
поведения не меняется: те же семь read-only тулов (ping,
weeek_get_me, weeek_list_projects, weeek_list_tasks,
weeek_get_task, weeek_list_members, weeek_list_tags) за теми же
тремя композирующимися server-side гейтами (READ_ONLY,
ENABLED_TOOLS, MAX_RESPONSE_CHARS), та же протокольная цель MCP
2025-06-18. Следом — I7 (read-side parity + многозначные
assignee + live-integration harness). Mutating tools приземляются в
I8. Полная мета-roadmap:
docs/reviews/roadmap.md.
Quickstart
npm install
npm run build
npm run inspect # запускает MCP Inspector против dist/index.jsДля smoke-теста без MCP-клиента см. docs/smoke.md (на английском).
Что сервер делает сегодня
Целится в MCP-протокол
2025-06-18поверх stdio через@modelcontextprotocol/sdk@^1.29.Обрабатывает:
initialize,notifications/initialized,tools/list,tools/call(все — собственность SDK).Регистрирует семь тулов; у каждого выставлены
annotations(readOnlyHint,destructiveHint,idempotentHint,openWorldHint) иoutputSchema, поэтому клиенты получаютstructuredContent:ping { msg: string } → { reply: "pong: <msg>" }— health-check,openWorldHint: false.weeek_get_me {} → { id, email, name }— обращается к Weeek/user/me, чтобы подтвердить выданный токен.nameсобирается изfirstName + lastName; если оба пусты — fallback наemail.weeek_list_projects {} → { projects: [{ id, title, color, isPrivate }], truncated }— все проекты, видимые токену.weeek_list_tasks { projectId?, completed?, offset?, perPage? } → { tasks: […], hasMore, truncated }— одна страница задач с offset/perPage-пагинацией; следуйтеhasMore.truncatedнезависим: пагинация = «есть ещё страницы»; truncation = «сервер обрезал текущую страницу под byte-budget».weeek_get_task { id } → { id, title, description, completed, projectId, priority, type, truncated }— одна задача по id; неизвестный id всплывает какweeek_get_task failed: weeek_not_found. Если ответ превышаетMAX_RESPONSE_CHARS, сервер обрезаетdescriptionи добавляет маркер…[truncated].weeek_list_members {} → { members: [{ id, email, firstName, lastName }], truncated }— все участники workspace.weeek_list_tags {} → { tags: [{ id, title, color }], truncated }— все теги.
Все Weeek-тулы (
weeek_*) помеченыopenWorldHint: true(они ходят вapi.weeek.net); ошибки апстрима всплывают какisError: trueсо стабильным текстомweeek_<code>и никогда как MCP-протокольные ошибки.Логирует структурированный JSON только в stderr; stdout зарезервирован за JSON-RPC. Логгер рекурсивно редактирует ключи
accessToken,authorization,password,secret,tokenперед записью в stderr.
Скрипты
| Команда | Назначение |
|---|---|
| npm run dev | Запуск напрямую из TS через tsx (без сборки) |
| npm run build | Компиляция в dist/ |
| npm start | Запуск собранного dist/index.js |
| npm run typecheck | tsc --noEmit по src/** (production) |
| npm run typecheck:tests | tsc -p tsconfig.test.json --noEmit по src/** + tests/** (noEmit; npm run build не затрагивается) |
| npm run lint | eslint src tests --max-warnings 0 |
| npm test | Один прогон unit-тестов (vitest) |
| npm run test:watch | Vitest watch-режим |
| npm run coverage | Тесты с V8-coverage отчётом |
| npm run inspect | MCP Inspector против собранного бинарника |
Конфигурация
Вся runtime-конфигурация читается из окружения на старте и
валидируется через zod (см. src/config/env.ts). Невалидные значения
прерывают старт: сообщение в stderr и ненулевой exit-code.
| Переменная | Обязательная | Значение по умолчанию | Назначение |
|---|---|---|---|
| WEEEK_ACCESS_TOKEN | да | — | Персональный API-токен Weeek. Минимум 20 символов; пустые строки, whitespace/control-символы и документированные плейсхолдеры отклоняются на старте. |
| WEEEK_BASE_URL | нет | https://api.weeek.net/public/v1 | Базовый URL Weeek HTTP-клиента. Меняйте только под self-hosted прокси. |
| WEEEK_TIMEOUT_MS | нет | 30000 | Per-request таймаут (мс) для Weeek HTTP-клиента. Положительное целое. |
| READ_ONLY | нет | true | Server-side гейт поверх annotations.readOnlyHint. Когда true, любой тул, у которого readOnlyHint !== true, пропускается на этапе регистрации и не попадает в tools/list. Принимает true / false / 1 / 0 (нечувствительно к регистру); любое другое значение прерывает старт. На сегодня no-op (все 7 тулов read-only); secure-by-default установлен заранее, на момент прихода mutating tools (I8). |
| ENABLED_TOOLS | нет | (не задана = все включены) | Список имён тулов через запятую. Когда задан, ограничивает tools/list перечисленными именами (всё ещё пересекается с READ_ONLY). Пробелы обрезаются; дубликаты схлопываются; неизвестные имена дают WARN на старте. Пустые / только-пробельные значения, либо список, в результате которого зарегистрировано ноль тулов, прерывают старт ещё до открытия stdio. |
| MAX_RESPONSE_CHARS | нет | 65536 | Верхняя граница на JSON.stringify(structuredContent).length per tool response. Пять тулов (weeek_list_projects, weeek_list_tasks, weeek_list_members, weeek_list_tags, weeek_get_task) обрезают payload под бюджет и возвращают truncated: boolean в outputSchema; списки роняют trailing items, weeek_get_task обрезает description маркером …[truncated]. Минимум 1024, максимум 1000000; нечисловые / out-of-range значения прерывают старт. ping и weeek_get_me не затрагиваются (их payload меньше минимума бюджета). |
| LOG_LEVEL | нет | info | Порог логгера: debug, info, warn, error. Неизвестные значения откатываются на info. |
Гейт READ_ONLY работает на стороне сервера: скрытый тул не
регистрируется, не появляется в tools/list, и агент не может
попытаться его вызвать. Когда гейт пропускает регистрацию, в stderr
выводится строка tool gated by READ_ONLY с именем тула. Невалидные
значения (например, READ_ONLY=maybe) прерывают старт сообщением
invalid env: READ_ONLY: must be 'true' | 'false' | '1' | '0'.
ENABLED_TOOLS работает по той же модели: тул, чьё имя не в списке,
не регистрируется, и для каждого пропущенного тула логируется
tool gated by ENABLED_TOOLS. Неизвестные имена логируются как
unknown tool in ENABLED_TOOLS ровно один раз каждое — без прерывания
(allowlist часто ссылается на тулы из ещё не смерженной ветки). Если
после применения и READ_ONLY, и ENABLED_TOOLS зарегистрировано
ноль тулов, старт прерывается сообщением no tools registered:
combination of READ_ONLY and ENABLED_TOOLS hides every tool — сервер
не открывает stdio в таком состоянии.
MAX_RESPONSE_CHARS отрабатывает после возврата тула: если итоговый
structuredContent превышает бюджет, сервер роняет trailing items
из списка (или клипает weeek_get_task.description маркером
…[truncated]), выставляет truncated: true и эмитит ровно одну
строку response truncated в stderr с полями tool,
maxResponseChars, originalChars, truncatedChars (никаких
заголовков / id / описаний). При успешном ответе пяти gated-тулов
возвращается truncated: false, поэтому агент может рассматривать
поле как стабильный сигнал.
См. .env.example — копипастабельный шаблон. Сервер
не читает .env-файл сам: пробрасывайте переменные через
env-блок MCP-клиента (см. пример ниже) или через свой shell.
Подключение к Claude Desktop
Скопируйте examples/claude_desktop.mcp.json
в конфиг Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json
на macOS), заменив оба плейсхолдера /ABSOLUTE/PATH/TO/... на вывод
which node и абсолютный путь до dist/index.js этого репозитория.
Реальный WEEEK_ACCESS_TOKEN обязателен начиная с I3 — без него
сервер отказывается стартовать.
Подключение к Cursor
Cursor использует тот же JSON-формат, что и Claude Desktop. Откройте
~/.cursor/mcp.json (создайте, если его нет) и добавьте:
{
"mcpServers": {
"weeek": {
"command": "/ABSOLUTE/PATH/TO/node",
"args": ["/ABSOLUTE/PATH/TO/weeek-mcp/dist/index.js"],
"env": {
"LOG_LEVEL": "info",
"WEEEK_ACCESS_TOKEN": "YOUR_WEEEK_TOKEN_HERE",
"WEEEK_BASE_URL": "https://api.weeek.net/public/v1",
"WEEEK_TIMEOUT_MS": "30000",
"READ_ONLY": "true",
"MAX_RESPONSE_CHARS": "65536"
}
}
}
}Перезапустите Cursor. Замените оба плейсхолдера /ABSOLUTE/PATH/TO/...
на which node и абсолютный путь до dist/index.js этого репозитория.
Готовый файл лежит в examples/cursor.mcp.json. Аналог для Cline — examples/cline.mcp.json (тот же shape).
Чтобы ограничить агента подмножеством тулов, добавьте
"ENABLED_TOOLS": "weeek_get_me,weeek_list_projects" в env-блок.
Оставьте переменную незаданной — будут видны все тулы, прошедшие
READ_ONLY-гейт.
Troubleshooting
| Симптом | Вероятная причина | Решение |
|---|---|---|
| Сервер не появляется в клиенте | command указывает на node, который клиент не находит, или dist/index.js отсутствует / без exec-bit | Запустите npm run build и убедитесь, что ls -la dist/index.js показывает 0755. Используйте абсолютный путь из which node (см. Workaround для NVM ниже). |
| MCP server failed to start сразу при запуске | То же, плюс отсутствует node_modules | npm install && npm run build из корня репозитория. |
| invalid env: WEEEK_ACCESS_TOKEN: ... в stderr | Токен содержит пробелы / control-символы или совпадает с плейсхолдером your-weeek-token-here | Сгенерируйте реальный токен на https://app.weeek.net/ws/_/settings/apps/api и вставьте без обрамляющих пробелов / переводов строки. |
| invalid env: WEEEK_BASE_URL: ... | URL с не-http(s)-схемой или содержит user:pass@ | Используйте обычный https://api.weeek.net/public/v1; пробрасывайте креды через WEEEK_ACCESS_TOKEN. |
| EACCES / permission denied при запуске dist/index.js | postbuild-chmod не отработал (например, при копировании на другой хост) | chmod +x dist/index.js. |
| npm start работает, но клиент всё ещё не запускается | Клиент стартует под другим PATH, чем ваш shell | См. Workaround для NVM. |
Чтобы воспроизвести сервер вне клиента, запустите npm start
напрямую и наблюдайте stderr — те же JSON-логи появятся там.
Workaround для NVM
Claude Desktop и Cursor стартуют свой MCP-subprocess в
неинтерактивном shell, который не делает source ~/.nvm/nvm.sh,
поэтому голый "command": "node" молча валится, если Node стоит
через nvm. Два варианта:
Зашить абсолютный путь (рекомендуется). Запустите
which nodeв терминале и вставьте результат — обычно вида/Users/<you>/.nvm/versions/node/v20.18.0/bin/node. Обновляйте при переключении активной nvm-версии.Использовать wrapper-скрипт, делающий source nvm:
#!/usr/bin/env bash export NVM_DIR="$HOME/.nvm" # shellcheck disable=SC1091 . "$NVM_DIR/nvm.sh" exec node "$@"Сохраните как
/usr/local/bin/node-via-nvm,chmod +x, и укажите путь вcommand. Переживёт смену nvm-версий.
Симптом этой проблемы: лог клиента говорит spawn node ENOENT или
MCP server failed to start, при том что npm start в shell
работает.
Design notes
- Wire-уровень (JSON-RPC framing, handshake,
zod → JSON Schema, version negotiation) живёт в@modelcontextprotocol/sdk; мы только собираемMcpServer + StdioServerTransportвsrc/index.tsи регистрируем тулы. См. docs/mcp-spec-sync.md про целевую версию протокола, отслеживание SDK/spec drift и resync-процесс. - Прежняя hand-rolled-реализация сохранена в ветке
explainer/no-sdk-implementationдля справки. - Каждый тул обязан декларировать
annotations(чтобы MCP-клиенты не скатывались на destructive-default) и — для всего, возвращающего структурные данные —outputSchema. ГейтREAD_ONLYфильтрует на этапе регистрации поверхannotations.readOnlyHint: скрытые тулы не регистрируются вообще и не появляются вtools/list; SDK отклоняет вызов какTool not found, а не запускает сisError-маркером.
Roadmap (короткий)
- I2 — env/config loader (zod-validated) ✅
- I3 — Weeek HTTP-клиент +
weeek_get_me;WEEEK_ACCESS_TOKENобязателен ✅ - I4 —
weeek_list_projects,weeek_list_tasks,weeek_get_task,weeek_list_members,weeek_list_tags✅ - I5a — server-side
READ_ONLYгейт ✅ - I5b —
ENABLED_TOOLSallowlist ✅ - I5c —
MAX_RESPONSE_CHARStruncation gate ✅ - I6a — Vitest + unit-тесты на pure modules ✅
- I6b — handler-тесты на mocked Weeek client +
client.tsтесты ✅ - I6c — GitHub Actions CI на Node 20 + 22 + ESLint расширен на
tests/**✅ - I-pub — npm publish (
0.1.0-rc.1), RU README, публичныйROADMAP.md⏳ - I7 — read-side parity (
get_project,list_boards,list_board_columns), многозначный assignee, live-integration harness, проход по описаниям тулов - I7.5 — tolerant inner-field shaping для всех 10 endpoints (envelope всё ещё strict)
- I8 — write tools (
create_task,update_task,move_task,complete_task); первое live-упражнение I5aREAD_ONLY - I9 — release maturity (1.0.0): автоматический publish, MCP Registry, Dependabot, CodeQL, полная двуязычная документация
Полная мета-roadmap со scope, out-of-scope, acceptance gates,
рисками и open questions блокирующими каждый инкремент:
docs/reviews/roadmap.md (на английском).
