backlog-mcp-server 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.ja.md +24 -16
  2. package/README.md +26 -16
  3. package/build/auth/backlogAuthContext.d.ts +2 -0
  4. package/build/auth/backlogOAuthClient.d.ts +10 -0
  5. package/build/auth/backlogOAuthClient.js +3 -3
  6. package/build/auth/backlogOAuthConfig.d.ts +9 -0
  7. package/build/auth/bearerAuthMiddleware.d.ts +4 -0
  8. package/build/auth/oauthRoutes.d.ts +4 -0
  9. package/build/auth/oauthRoutes.js +12 -5
  10. package/build/auth/tokenStore.d.ts +62 -0
  11. package/build/backlog/backlogErrorHandler.d.ts +2 -0
  12. package/build/backlog/customFields.d.ts +29 -0
  13. package/build/backlog/parseBacklogAPIError.d.ts +8 -0
  14. package/build/createBacklogMcpServer.d.ts +29 -0
  15. package/build/createBacklogMcpServer.js +18 -6
  16. package/build/createTranslationHelper.d.ts +16 -0
  17. package/build/createTranslationHelper.js +7 -11
  18. package/build/handlers/builders/composeToolHandler.d.ts +31 -0
  19. package/build/handlers/builders/composeToolHandler.js +24 -9
  20. package/build/handlers/transformers/wrapWithErrorHandling.d.ts +2 -0
  21. package/build/handlers/transformers/wrapWithFieldPicking.d.ts +4 -0
  22. package/build/handlers/transformers/wrapWithOrganizationContext.d.ts +3 -0
  23. package/build/handlers/transformers/wrapWithTokenLimit.d.ts +2 -0
  24. package/build/handlers/transformers/wrapWithToolResult.d.ts +6 -0
  25. package/build/httpMcpServer.d.ts +22 -0
  26. package/build/httpMcpServer.js +46 -100
  27. package/build/index.d.ts +2 -0
  28. package/build/index.js +33 -10
  29. package/build/lib.d.ts +25 -0
  30. package/build/lib.js +20 -0
  31. package/build/loadTranslationOverrides.d.ts +17 -0
  32. package/build/loadTranslationOverrides.js +24 -0
  33. package/build/registerTools.d.ts +5 -0
  34. package/build/registerTools.js +7 -6
  35. package/build/tools/addDocument.d.ts +15 -0
  36. package/build/tools/addIssue.d.ts +30 -0
  37. package/build/tools/addIssueComment.d.ts +14 -0
  38. package/build/tools/addProject.d.ts +18 -0
  39. package/build/tools/addPullRequest.d.ts +20 -0
  40. package/build/tools/addPullRequestComment.d.ts +16 -0
  41. package/build/tools/addRelatedIssue.d.ts +12 -0
  42. package/build/tools/addVersionMilestone.d.ts +15 -0
  43. package/build/tools/addWatching.d.ts +11 -0
  44. package/build/tools/addWiki.d.ts +13 -0
  45. package/build/tools/countIssues.d.ts +48 -0
  46. package/build/tools/deleteIssue.d.ts +11 -0
  47. package/build/tools/deleteProject.d.ts +11 -0
  48. package/build/tools/deleteVersion.d.ts +12 -0
  49. package/build/tools/deleteWatching.d.ts +10 -0
  50. package/build/tools/dynamicTools/organizations.d.ts +6 -0
  51. package/build/tools/dynamicTools/toolsets.d.ts +15 -0
  52. package/build/tools/getCategories.d.ts +11 -0
  53. package/build/tools/getCustomFields.d.ts +12 -0
  54. package/build/tools/getDocument.d.ts +10 -0
  55. package/build/tools/getDocumentTree.d.ts +10 -0
  56. package/build/tools/getDocuments.d.ts +11 -0
  57. package/build/tools/getGitRepositories.d.ts +11 -0
  58. package/build/tools/getGitRepository.d.ts +13 -0
  59. package/build/tools/getIssue.d.ts +11 -0
  60. package/build/tools/getIssueComments.d.ts +18 -0
  61. package/build/tools/getIssueTypes.d.ts +11 -0
  62. package/build/tools/getIssues.d.ts +75 -0
  63. package/build/tools/getMyself.d.ts +7 -0
  64. package/build/tools/getNotifications.d.ts +16 -0
  65. package/build/tools/getNotificationsCount.d.ts +11 -0
  66. package/build/tools/getPriorities.d.ts +7 -0
  67. package/build/tools/getProject.d.ts +11 -0
  68. package/build/tools/getProjectList.d.ts +11 -0
  69. package/build/tools/getProjectUsers.d.ts +11 -0
  70. package/build/tools/getPullRequest.d.ts +14 -0
  71. package/build/tools/getPullRequestComments.d.ts +21 -0
  72. package/build/tools/getPullRequests.d.ts +19 -0
  73. package/build/tools/getPullRequestsCount.d.ts +17 -0
  74. package/build/tools/getRelatedIssues.d.ts +11 -0
  75. package/build/tools/getResolutions.d.ts +7 -0
  76. package/build/tools/getSpace.d.ts +7 -0
  77. package/build/tools/getSpaceActivities.d.ts +45 -0
  78. package/build/tools/getUserRecentUpdates.d.ts +46 -0
  79. package/build/tools/getUserStarsCount.d.ts +12 -0
  80. package/build/tools/getUsers.d.ts +7 -0
  81. package/build/tools/getVersionMilestoneList.d.ts +11 -0
  82. package/build/tools/getWatchingListCount.d.ts +10 -0
  83. package/build/tools/getWatchingListItems.d.ts +10 -0
  84. package/build/tools/getWiki.d.ts +10 -0
  85. package/build/tools/getWikiPages.d.ts +12 -0
  86. package/build/tools/getWikisCount.d.ts +11 -0
  87. package/build/tools/markNotificationAsRead.d.ts +13 -0
  88. package/build/tools/markWatchingAsRead.d.ts +13 -0
  89. package/build/tools/removeRelatedIssue.d.ts +12 -0
  90. package/build/tools/resetUnreadNotificationCount.d.ts +7 -0
  91. package/build/tools/shared/customFieldFiltersSchema.d.ts +21 -0
  92. package/build/tools/tools.d.ts +4 -0
  93. package/build/tools/updateIssue.d.ts +34 -0
  94. package/build/tools/updateIssue.js +31 -4
  95. package/build/tools/updateIssueComment.d.ts +13 -0
  96. package/build/tools/updateProject.d.ts +21 -0
  97. package/build/tools/updatePullRequest.d.ts +20 -0
  98. package/build/tools/updatePullRequestComment.d.ts +16 -0
  99. package/build/tools/updateVersionMilestone.d.ts +17 -0
  100. package/build/tools/updateWatching.d.ts +11 -0
  101. package/build/tools/updateWiki.d.ts +13 -0
  102. package/build/types/mcp.d.ts +10 -0
  103. package/build/types/result.d.ts +10 -0
  104. package/build/types/tool.d.ts +24 -0
  105. package/build/types/toolsets.d.ts +16 -0
  106. package/build/types/zod/backlogOutputDefinition.d.ts +3334 -0
  107. package/build/types/zod/backlogOutputDefinition.js +3 -16
  108. package/build/utils/backlogClientRegistry.d.ts +28 -0
  109. package/build/utils/backlogClientRegistry.js +4 -0
  110. package/build/utils/backlogOrganizationContext.d.ts +2 -0
  111. package/build/utils/generateFieldsDescription.d.ts +5 -0
  112. package/build/utils/generateFieldsDescription.js +3 -0
  113. package/build/utils/logger.d.ts +2 -0
  114. package/build/utils/logger.js +27 -14
  115. package/build/utils/resolveIdOrKey.d.ts +18 -0
  116. package/build/utils/runToolSafely.d.ts +6 -0
  117. package/build/utils/tokenCounter.d.ts +1 -0
  118. package/build/utils/toolRegistrar.d.ts +5 -0
  119. package/build/utils/toolsetUtils.d.ts +6 -0
  120. package/build/utils/toolsetUtils.js +0 -20
  121. package/build/utils/wrapServerWithToolRegistry.d.ts +6 -0
  122. package/build/utils/wrapServerWithToolRegistry.js +1 -1
  123. package/package.json +15 -5
package/README.ja.md CHANGED
@@ -154,11 +154,11 @@ MCPサーバーをネットワーク経由で公開する場合、OAuth 2.0認
154
154
 
155
155
  2. 以下の環境変数を設定します(`BACKLOG_DOMAIN` に加えて):
156
156
 
157
- | 変数 | 説明 |
158
- | ---- | ---- |
159
- | `BACKLOG_OAUTH_CLIENT_ID` | Backlogアプリケーションの OAuth Client ID |
160
- | `BACKLOG_OAUTH_CLIENT_SECRET` | Backlogアプリケーションの OAuth Client Secret |
161
- | `MCP_SERVER_BASE_URL` | MCPサーバーの公開URL(例:`https://mcp.example.com`) |
157
+ | 変数 | 説明 |
158
+ | ----------------------------- | ----------------------------------------------------- |
159
+ | `BACKLOG_OAUTH_CLIENT_ID` | Backlogアプリケーションの OAuth Client ID |
160
+ | `BACKLOG_OAUTH_CLIENT_SECRET` | Backlogアプリケーションの OAuth Client Secret |
161
+ | `MCP_SERVER_BASE_URL` | MCPサーバーの公開URL(例:`https://mcp.example.com`) |
162
162
 
163
163
  > **注意:** OAuth有効時は `BACKLOG_API_KEY` は**不要**です。各ユーザーが自分のBacklogアカウントで認証します。
164
164
 
@@ -169,23 +169,27 @@ BACKLOG_DOMAIN=your-space.backlog.com \
169
169
  BACKLOG_OAUTH_CLIENT_ID=your-client-id \
170
170
  BACKLOG_OAUTH_CLIENT_SECRET=your-client-secret \
171
171
  MCP_SERVER_BASE_URL=https://mcp.example.com \
172
- node build/index.js --transport http --http-host 0.0.0.0 --http-port 3333
172
+ node build/index.js --transport http --http-host 0.0.0.0 --http-port 3333 \
173
+ --http-allowed-hosts mcp.example.com
173
174
  ```
174
175
 
176
+ `0.0.0.0` にバインドする場合、`--http-allowed-hosts` は実質必須です。指定しないとDNSリバインディング保護が一切効かず、起動時に警告がログ出力されます。
177
+
175
178
  OAuth有効時、サーバーは以下のOAuthエンドポイントを自動的に公開します:
176
179
 
177
- | エンドポイント | 説明 |
178
- | ------------- | ---- |
179
- | `GET /.well-known/oauth-authorization-server` | OAuth認可サーバーメタデータ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) |
180
- | `GET /.well-known/oauth-protected-resource/mcp` | OAuthリソースメタデータ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) |
181
- | `POST /register` | 動的クライアント登録([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)) |
182
- | `GET /authorize` | 認可エンドポイント(Backlog OAuthへリダイレクト) |
183
- | `GET /callback` | Backlog OAuthコールバック |
184
- | `POST /token` | トークンエンドポイント(認可コード&リフレッシュトークン) |
180
+ | エンドポイント | 説明 |
181
+ | ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
182
+ | `GET /.well-known/oauth-authorization-server` | OAuth認可サーバーメタデータ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) |
183
+ | `GET /.well-known/oauth-protected-resource/mcp` | OAuthリソースメタデータ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) |
184
+ | `POST /register` | 動的クライアント登録([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)) |
185
+ | `GET /authorize` | 認可エンドポイント(Backlog OAuthへリダイレクト) |
186
+ | `GET /callback` | Backlog OAuthコールバック |
187
+ | `POST /token` | トークンエンドポイント(認可コード&リフレッシュトークン) |
185
188
 
186
189
  MCP認可仕様に対応するMCPクライアントは、これらのエンドポイントを自動的に使用します。
187
190
 
188
191
  > **制約事項:**
192
+ >
189
193
  > - OAuthモードは現在、単一のBacklog組織のみをサポートしています。複数組織設定との併用はできません。
190
194
  > - クライアント登録やトークンはメモリ内に保持されるため、サーバー再起動時に失われます。
191
195
 
@@ -247,6 +251,8 @@ CLI経由での有効化:
247
251
 
248
252
  動的ツールセットを有効にすると、LLMはツールインターフェースを介してオンデマンドでツールセットを一覧表示およびアクティブ化できるようになります。
249
253
 
254
+ > **HTTPでのスコープ:** MCP `2026-07-28` にはプロトコルセッションがないため、アクティブ化したツールセットはクライアント単位ではなく**サーバープロセス単位**で保持されます。HTTPトランスポートでは接続中の全クライアントが1つのツールセット状態を共有し、プロセス再起動でリセットされます。共有されるのはツールの*可視性*のみで、認可は共有されません(各呼び出しは呼び出し元自身の認証情報で認証されます)。
255
+
250
256
  ## 利用可能なツール
251
257
 
252
258
  以下のような Backlog 機能に対応するツールを提供しています:
@@ -590,7 +596,9 @@ BACKLOG_API_KEY=your-api-key
590
596
 
591
597
  ### ツールの使い方
592
598
 
593
- 通常のツールはすべて、任意の`organization`入力フィールドを受け付けます。指定した場合、そのBacklog組織に対してツールが実行されます。
599
+ マルチ組織用の環境変数が設定されている場合、通常のツールはすべて任意の`organization`入力フィールドを受け付けます。指定した場合、そのBacklog組織に対してツールが実行されます。
600
+
601
+ 単一組織モードでは、ルーティング先が1つしかないためこのフィールドは公開されません。これにより`tools/list`のレスポンスから約8KBのスキーマが削減されます。
594
602
 
595
603
  例:
596
604
 
@@ -608,7 +616,7 @@ BACKLOG_API_KEY=your-api-key
608
616
 
609
617
  ### 組織一覧の確認
610
618
 
611
- サーバーは `list_organizations` ツールを提供しており、設定済みの組織名、ドメイン、デフォルト組織かどうかを返します。
619
+ マルチ組織モードでは、サーバーは `list_organizations` ツールを提供しており、設定済みの組織名、ドメイン、デフォルト組織かどうかを返します。単一組織モードでは登録されません。
612
620
 
613
621
  レスポンス例:
614
622
 
package/README.md CHANGED
@@ -149,20 +149,22 @@ pnpm run build
149
149
  MCP_TRANSPORT=http MCP_HTTP_PORT=3333 node build/index.js
150
150
  ```
151
151
 
152
- - **Endpoint:** `POST`, `GET`, and `DELETE` on `http://<host>:<port><path>` (default path `/mcp`).
153
- - **Session:** After `initialize`, clients must send the `mcp-session-id` header on later requests (as returned by the server).
154
- - **Security:** Default bind is `127.0.0.1`. Do not expose the HTTP port to untrusted networks without authentication and TLS; it allows full use of your Backlog API key via MCP tools.
152
+ - **Endpoint:** `POST` (and `GET` for server-initiated streams) on `http://<host>:<port><path>` (default path `/mcp`).
153
+ - **Protocol:** MCP `2026-07-28`. The protocol is stateless: there is no `initialize` handshake and no `mcp-session-id` header. Clients send their metadata in `_meta` on every request and discover capabilities via `server/discover`. Streamable HTTP also requires the `Mcp-Method` header (and `Mcp-Name` on `tools/call`).
154
+ - **Backward compatibility:** Clients on `2025-11-25` and earlier are still served over the same endpoint, statelessly. Because no session is kept, the 2025 session operations (`GET` / `DELETE` with an `mcp-session-id`) answer `405`.
155
+ - **Security:** Default bind is `127.0.0.1`. On a bare loopback bind, `Host` and `Origin` are both validated against the localhost set (DNS rebinding protection). Behind a reverse proxy, set `--http-allowed-hosts` to the public hostname; that turns off the localhost `Origin` default, since a browser client's `Origin` is its own site and never this server's hostname. Add `--http-allowed-origins` to restrict which client origins may reach the server. Do not expose the HTTP port to untrusted networks without authentication and TLS; it allows full use of your Backlog API key via MCP tools.
155
156
 
156
157
  Environment variables (CLI flags override when both are set):
157
158
 
158
- | Variable | Description |
159
- | ------------------------ | ------------------------------------------------------------------------------------------ |
160
- | `MCP_TRANSPORT` | `stdio` (default) or `http` |
161
- | `MCP_HTTP_HOST` | Bind address (default `127.0.0.1`) |
162
- | `MCP_HTTP_PORT` | Port (default `3333`) |
163
- | `MCP_HTTP_PATH` | URL path (default `/mcp`) |
164
- | `MCP_HTTP_JSON_RESPONSE` | `true` to prefer JSON responses over SSE when supported |
165
- | `MCP_HTTP_ALLOWED_HOSTS` | Comma-separated allowed `Host` values when binding to `0.0.0.0` (DNS rebinding protection) |
159
+ | Variable | Description |
160
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `MCP_TRANSPORT` | `stdio` (default) or `http` |
162
+ | `MCP_HTTP_HOST` | Bind address (default `127.0.0.1`) |
163
+ | `MCP_HTTP_PORT` | Port (default `3333`) |
164
+ | `MCP_HTTP_PATH` | URL path (default `/mcp`) |
165
+ | `MCP_HTTP_JSON_RESPONSE` | `true` to prefer JSON responses over SSE (applies to `2026-07-28` clients only) |
166
+ | `MCP_HTTP_ALLOWED_HOSTS` | Comma-separated allowed `Host` hostnames (port-agnostic). Required when binding to `0.0.0.0`; also the escape hatch for a loopback bind behind a proxy (DNS rebinding protection) |
167
+ | `MCP_HTTP_ALLOWED_ORIGINS` | Comma-separated allowed `Origin` hostnames for browser-based clients. Defaults to the localhost set on a bare loopback bind, and to no `Origin` check otherwise |
166
168
 
167
169
  ### OAuth 2.0 Authentication (Remote MCP)
168
170
 
@@ -194,9 +196,12 @@ BACKLOG_DOMAIN=your-space.backlog.com \
194
196
  BACKLOG_OAUTH_CLIENT_ID=your-client-id \
195
197
  BACKLOG_OAUTH_CLIENT_SECRET=your-client-secret \
196
198
  MCP_SERVER_BASE_URL=https://mcp.example.com \
197
- node build/index.js --transport http --http-host 0.0.0.0 --http-port 3333
199
+ node build/index.js --transport http --http-host 0.0.0.0 --http-port 3333 \
200
+ --http-allowed-hosts mcp.example.com
198
201
  ```
199
202
 
203
+ `--http-allowed-hosts` is required in practice when binding to `0.0.0.0`: without it there is no DNS rebinding protection, and the server logs a warning at startup.
204
+
200
205
  The server automatically exposes the following OAuth endpoints when OAuth is enabled:
201
206
 
202
207
  | Endpoint | Description |
@@ -273,6 +278,8 @@ Or via environment variable::
273
278
 
274
279
  With dynamic toolsets enabled, the LLM will be able to list and activate toolsets on demand via tool interface.
275
280
 
281
+ > **Scope over HTTP:** MCP `2026-07-28` has no protocol sessions, so an activated toolset is remembered per **server process**, not per client. On the HTTP transport every connected client shares one toolset state, and it resets when the process restarts. Tool _visibility_ is shared; authorization is not — every call is still authenticated with the caller's own credentials.
282
+
276
283
  ## Available Tools
277
284
 
278
285
  ### Toolset: `space`
@@ -644,8 +651,9 @@ The server supports several command line options:
644
651
 
645
652
  - `--transport stdio|http`: MCP transport (default: stdio). Use `http` for Streamable HTTP.
646
653
  - `--http-host`, `--http-port`, `--http-path`: HTTP bind address, port, and path (defaults: `127.0.0.1`, `3333`, `/mcp`).
647
- - `--http-json-response`: Prefer JSON responses over SSE when the transport supports it.
648
- - `--http-allowed-hosts`: Comma-separated allowed `Host` headers when binding to all interfaces.
654
+ - `--http-json-response`: Prefer JSON responses over SSE. Applies to `2026-07-28` clients only; the backward-compatible `2025-11-25` path is served with the SDK's default response shaping.
655
+ - `--http-allowed-hosts`: Comma-separated allowed `Host` hostnames (port-agnostic). Needed when binding to all interfaces, or on a loopback bind behind a reverse proxy.
656
+ - `--http-allowed-origins`: Comma-separated allowed `Origin` hostnames for browser-based clients. Defaults to the localhost set on a bare loopback bind, and to no `Origin` check otherwise.
649
657
  - `--export-translations`: Export all translation keys and values
650
658
  - `--optimize-response`: Enable GraphQL-style field selection
651
659
  - `--max-tokens=NUMBER`: Set maximum token limit for responses
@@ -707,7 +715,9 @@ BACKLOG_API_KEY=your-api-key
707
715
 
708
716
  ### Tool Usage
709
717
 
710
- All normal tools accept an optional `organization` input field. When provided, the tool call is routed to that Backlog organization.
718
+ When multi-organization env vars are configured, all normal tools accept an optional `organization` input field. When provided, the tool call is routed to that Backlog organization.
719
+
720
+ In single-organization mode the field is not published, since there would be only one organization to route to. Omitting it keeps roughly 8 KB of tool schema out of every `tools/list` response.
711
721
 
712
722
  Examples:
713
723
 
@@ -725,7 +735,7 @@ If `organization` is omitted:
725
735
 
726
736
  ### Organization Discovery
727
737
 
728
- The server provides a `list_organizations` tool that returns the configured organization names, their domains, and which one is the default.
738
+ In multi-organization mode the server provides a `list_organizations` tool that returns the configured organization names, their domains, and which one is the default. It is not registered in single-organization mode.
729
739
 
730
740
  Example response:
731
741
 
@@ -0,0 +1,2 @@
1
+ export declare function runWithAccessToken<T>(token: string | undefined, fn: () => Promise<T>): Promise<T>;
2
+ export declare function getCurrentAccessToken(): string | undefined;
@@ -0,0 +1,10 @@
1
+ import type { BacklogOAuthConfig } from './backlogOAuthConfig.js';
2
+ import type { BacklogTokenData } from './tokenStore.js';
3
+ export declare function buildBacklogAuthorizationUrl(config: BacklogOAuthConfig, redirectUri: string, state: string): string;
4
+ export declare function exchangeBacklogCode(config: BacklogOAuthConfig, code: string, redirectUri: string): Promise<BacklogTokenData>;
5
+ export declare function refreshBacklogToken(config: BacklogOAuthConfig, refreshToken: string): Promise<BacklogTokenData>;
6
+ export declare function verifyBacklogToken(domain: string, accessToken: string): Promise<{
7
+ id: number;
8
+ userId: string;
9
+ name: string;
10
+ }>;
@@ -26,7 +26,7 @@ export async function exchangeBacklogCode(config, code, redirectUri) {
26
26
  const text = await response.text();
27
27
  throw new Error(`Backlog token exchange failed (${response.status}): ${text}`);
28
28
  }
29
- return (await response.json());
29
+ return await response.json();
30
30
  }
31
31
  export async function refreshBacklogToken(config, refreshToken) {
32
32
  const params = new URLSearchParams({
@@ -44,7 +44,7 @@ export async function refreshBacklogToken(config, refreshToken) {
44
44
  const text = await response.text();
45
45
  throw new Error(`Backlog token refresh failed (${response.status}): ${text}`);
46
46
  }
47
- return (await response.json());
47
+ return await response.json();
48
48
  }
49
49
  export async function verifyBacklogToken(domain, accessToken) {
50
50
  const response = await fetch(`https://${domain}/api/v2/users/myself`, {
@@ -53,5 +53,5 @@ export async function verifyBacklogToken(domain, accessToken) {
53
53
  if (!response.ok) {
54
54
  throw new Error(`Backlog token verification failed (${response.status})`);
55
55
  }
56
- return (await response.json());
56
+ return await response.json();
57
57
  }
@@ -0,0 +1,9 @@
1
+ export type BacklogOAuthConfig = {
2
+ clientId: string;
3
+ clientSecret: string;
4
+ backlogDomain: string;
5
+ serverBaseUrl: string;
6
+ };
7
+ type Environment = Record<string, string | undefined>;
8
+ export declare function getBacklogOAuthConfig(env?: Environment): BacklogOAuthConfig | undefined;
9
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { BacklogOAuthConfig } from './backlogOAuthConfig.js';
3
+ import type { TokenStore } from './tokenStore.js';
4
+ export declare function createBearerAuthMiddleware(store: TokenStore, config: BacklogOAuthConfig, mcpPath: string): MiddlewareHandler;
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import type { BacklogOAuthConfig } from './backlogOAuthConfig.js';
3
+ import type { TokenStore } from './tokenStore.js';
4
+ export declare function createOAuthRoutes(config: BacklogOAuthConfig, store: TokenStore, mcpPath: string): Hono;
@@ -20,10 +20,7 @@ function isValidRedirectUri(uri) {
20
20
  const parsed = new URL(uri);
21
21
  if (parsed.protocol === 'https:')
22
22
  return true;
23
- if (parsed.protocol === 'http:' &&
24
- LOCALHOST_HOSTS.includes(parsed.hostname))
25
- return true;
26
- return false;
23
+ return (parsed.protocol === 'http:' && LOCALHOST_HOSTS.includes(parsed.hostname));
27
24
  }
28
25
  catch {
29
26
  return false;
@@ -46,6 +43,8 @@ export function createOAuthRoutes(config, store, mcpPath) {
46
43
  grant_types_supported: ['authorization_code', 'refresh_token'],
47
44
  token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
48
45
  code_challenge_methods_supported: ['S256'],
46
+ // RFC 9207 — every authorization response carries `iss`
47
+ authorization_response_iss_parameter_supported: true,
49
48
  });
50
49
  });
51
50
  // RFC 9728 — OAuth Protected Resource Metadata
@@ -61,7 +60,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
61
60
  app.post('/register', async (c) => {
62
61
  let body;
63
62
  try {
64
- body = (await c.req.json());
63
+ body = await c.req.json();
65
64
  }
66
65
  catch {
67
66
  return c.json(oauthError('invalid_request', 'Invalid JSON body'), 400);
@@ -136,6 +135,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
136
135
  const url = new URL(effectiveRedirectUri);
137
136
  url.searchParams.set('error', 'unsupported_response_type');
138
137
  url.searchParams.set('error_description', 'Only response_type=code is supported');
138
+ url.searchParams.set('iss', serverBaseUrl);
139
139
  if (state)
140
140
  url.searchParams.set('state', state);
141
141
  return c.redirect(url.href, 302);
@@ -144,6 +144,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
144
144
  const url = new URL(effectiveRedirectUri);
145
145
  url.searchParams.set('error', 'invalid_request');
146
146
  url.searchParams.set('error_description', 'code_challenge is required');
147
+ url.searchParams.set('iss', serverBaseUrl);
147
148
  if (state)
148
149
  url.searchParams.set('state', state);
149
150
  return c.redirect(url.href, 302);
@@ -152,6 +153,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
152
153
  const url = new URL(effectiveRedirectUri);
153
154
  url.searchParams.set('error', 'invalid_request');
154
155
  url.searchParams.set('error_description', 'Only code_challenge_method=S256 is supported');
156
+ url.searchParams.set('iss', serverBaseUrl);
155
157
  if (state)
156
158
  url.searchParams.set('state', state);
157
159
  return c.redirect(url.href, 302);
@@ -160,6 +162,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
160
162
  const url = new URL(effectiveRedirectUri);
161
163
  url.searchParams.set('error', 'invalid_target');
162
164
  url.searchParams.set('error_description', 'Invalid resource parameter');
165
+ url.searchParams.set('iss', serverBaseUrl);
163
166
  if (state)
164
167
  url.searchParams.set('state', state);
165
168
  return c.redirect(url.href, 302);
@@ -196,6 +199,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
196
199
  errorUrl.searchParams.set('error', backlogError ?? 'access_denied');
197
200
  errorUrl.searchParams.set('error_description', url.searchParams.get('error_description') ??
198
201
  'Authorization was denied by the user');
202
+ errorUrl.searchParams.set('iss', serverBaseUrl);
199
203
  if (pending.state)
200
204
  errorUrl.searchParams.set('state', pending.state);
201
205
  return c.redirect(errorUrl.href, 302);
@@ -213,6 +217,7 @@ export function createOAuthRoutes(config, store, mcpPath) {
213
217
  const url = new URL(pending.redirectUri);
214
218
  url.searchParams.set('error', 'server_error');
215
219
  url.searchParams.set('error_description', 'Failed to exchange authorization code with Backlog');
220
+ url.searchParams.set('iss', serverBaseUrl);
216
221
  if (pending.state)
217
222
  url.searchParams.set('state', pending.state);
218
223
  return c.redirect(url.href, 302);
@@ -228,6 +233,8 @@ export function createOAuthRoutes(config, store, mcpPath) {
228
233
  });
229
234
  const redirectUrl = new URL(pending.redirectUri);
230
235
  redirectUrl.searchParams.set('code', mcpCode);
236
+ // RFC 9207 — let the client confirm which AS answered
237
+ redirectUrl.searchParams.set('iss', serverBaseUrl);
231
238
  if (pending.state)
232
239
  redirectUrl.searchParams.set('state', pending.state);
233
240
  return c.redirect(redirectUrl.href, 302);
@@ -0,0 +1,62 @@
1
+ import type { AuthInfo } from '@modelcontextprotocol/server';
2
+ export type BacklogTokenData = {
3
+ access_token: string;
4
+ token_type: string;
5
+ expires_in: number;
6
+ refresh_token: string;
7
+ };
8
+ export type OAuthClientInfo = {
9
+ client_id: string;
10
+ client_secret?: string;
11
+ client_id_issued_at: number;
12
+ client_secret_expires_at: number;
13
+ redirect_uris: string[];
14
+ client_name?: string;
15
+ token_endpoint_auth_method?: string;
16
+ grant_types?: string[];
17
+ response_types?: string[];
18
+ };
19
+ type PendingAuthorization = {
20
+ mcpClientId: string;
21
+ codeChallenge: string;
22
+ redirectUri: string;
23
+ resource?: string;
24
+ scopes: string[];
25
+ state?: string;
26
+ createdAt: number;
27
+ };
28
+ type AuthCodeEntry = {
29
+ mcpClientId: string;
30
+ backlogTokens: BacklogTokenData;
31
+ codeChallenge: string;
32
+ redirectUri: string;
33
+ resource?: string;
34
+ expiresAt: number;
35
+ };
36
+ export type McpTokenEntry = {
37
+ backlogAccessToken: string;
38
+ clientId: string;
39
+ expiresAt: number;
40
+ };
41
+ type McpRefreshEntry = {
42
+ backlogRefreshToken: string;
43
+ clientId: string;
44
+ expiresAt: number;
45
+ };
46
+ export type TokenStore = ReturnType<typeof createTokenStore>;
47
+ export declare function createTokenStore(): {
48
+ storePendingAuth(backlogState: string, pending: PendingAuthorization): void;
49
+ consumePendingAuth(backlogState: string): PendingAuthorization | undefined;
50
+ storeAuthCode(code: string, entry: AuthCodeEntry): void;
51
+ consumeAuthCode(code: string): AuthCodeEntry | undefined;
52
+ getClient(clientId: string): OAuthClientInfo | undefined;
53
+ registerClient(client: OAuthClientInfo): boolean;
54
+ getCachedVerification(token: string): AuthInfo | undefined;
55
+ cacheVerification(token: string, authInfo: AuthInfo, ttlMs: number): void;
56
+ storeMcpToken(mcpToken: string, entry: McpTokenEntry): void;
57
+ getMcpToken(mcpToken: string): McpTokenEntry | undefined;
58
+ storeMcpRefreshToken(mcpRefreshToken: string, entry: McpRefreshEntry): void;
59
+ consumeMcpRefreshToken(mcpRefreshToken: string): McpRefreshEntry | undefined;
60
+ cleanup(): void;
61
+ };
62
+ export {};
@@ -0,0 +1,2 @@
1
+ import { ErrorLike } from '../types/result.js';
2
+ export declare const backlogErrorHandler: (err: unknown) => ErrorLike;
@@ -0,0 +1,29 @@
1
+ export type CustomFieldInput = {
2
+ id: number;
3
+ value?: string | number | string[] | number[];
4
+ otherValue?: string;
5
+ };
6
+ export type CustomFieldFilterInput = {
7
+ id: number;
8
+ type: 'text';
9
+ value: string;
10
+ } | {
11
+ id: number;
12
+ type: 'numeric';
13
+ min?: number;
14
+ max?: number;
15
+ } | {
16
+ id: number;
17
+ type: 'date';
18
+ min?: string;
19
+ max?: string;
20
+ } | {
21
+ id: number;
22
+ type: 'list';
23
+ value: number | number[];
24
+ };
25
+ /**
26
+ * Converts Backlog-style customFields array into proper payload format
27
+ */
28
+ export declare function customFieldsToPayload(customFields: CustomFieldInput[] | undefined): Record<string, string | number | string[] | number[] | undefined>;
29
+ export declare function customFieldFiltersToPayload(customFields: CustomFieldFilterInput[] | undefined): Record<string, string | number | number[] | undefined>;
@@ -0,0 +1,8 @@
1
+ export type ParsedBacklogAPIError = {
2
+ type: 'BacklogAuthError' | 'BacklogApiError' | 'UnexpectedError' | 'UnknownError';
3
+ message: string;
4
+ status?: number;
5
+ code?: number;
6
+ url?: string;
7
+ };
8
+ export declare function parseBacklogAPIError(err: unknown): ParsedBacklogAPIError;
@@ -0,0 +1,29 @@
1
+ import type { Backlog } from 'backlog-js';
2
+ import type { TranslationHelper } from './createTranslationHelper.js';
3
+ import type { MCPOptions } from './types/mcp.js';
4
+ import type { ToolsetGroup } from './types/toolsets.js';
5
+ import type { BacklogClientRegistry } from './utils/backlogClientRegistry.js';
6
+ import { type BacklogMCPServer } from './utils/wrapServerWithToolRegistry.js';
7
+ export type CreateBacklogMcpServerConfig = {
8
+ version: string;
9
+ useFields: boolean;
10
+ backlog: Backlog;
11
+ clientRegistry: BacklogClientRegistry;
12
+ transHelper: TranslationHelper;
13
+ enabledToolsets: string[];
14
+ mcpOption: MCPOptions;
15
+ dynamicToolsets: boolean;
16
+ /**
17
+ * A toolset group to register from, instead of building one from
18
+ * `enabledToolsets`. Callers that produce many servers from one factory pass
19
+ * a single shared group: `enable_toolset` mutates it, and under the stateless
20
+ * HTTP model — one server per request — a group built per server would throw
21
+ * that mutation away the moment the request ends.
22
+ */
23
+ toolsetGroup?: ToolsetGroup;
24
+ };
25
+ /**
26
+ * Builds a fresh MCP server instance with all Backlog tools registered.
27
+ * Used once per stdio connection; one instance per HTTP request for Streamable HTTP.
28
+ */
29
+ export declare function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, transHelper, enabledToolsets, mcpOption, dynamicToolsets, toolsetGroup: sharedToolsetGroup, }: CreateBacklogMcpServerConfig): BacklogMCPServer;
@@ -1,25 +1,37 @@
1
1
  // Copyright (c) 2025 Nulab inc.
2
2
  // Licensed under the MIT License.
3
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { McpServer } from '@modelcontextprotocol/server';
4
4
  import { registerDynamicTools, registerTools } from './registerTools.js';
5
5
  import { organizationTools } from './tools/dynamicTools/organizations.js';
6
6
  import { dynamicTools } from './tools/dynamicTools/toolsets.js';
7
7
  import { createToolRegistrar } from './utils/toolRegistrar.js';
8
8
  import { buildToolsetGroup } from './utils/toolsetUtils.js';
9
9
  import { wrapServerWithToolRegistry, } from './utils/wrapServerWithToolRegistry.js';
10
+ // The tool list is fixed for the process lifetime (it only depends on CLI flags
11
+ // and environment), so clients may cache it. With dynamic toolsets the list can
12
+ // grow at runtime, so no hint is published in that case.
13
+ const TOOL_LIST_CACHE_HINT = {
14
+ 'tools/list': { ttlMs: 5 * 60 * 1000, cacheScope: 'public' },
15
+ };
10
16
  /**
11
17
  * Builds a fresh MCP server instance with all Backlog tools registered.
12
- * Used once for stdio; one instance per HTTP session for Streamable HTTP.
18
+ * Used once per stdio connection; one instance per HTTP request for Streamable HTTP.
13
19
  */
14
- export function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, transHelper, enabledToolsets, mcpOption, dynamicToolsets, }) {
20
+ export function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, transHelper, enabledToolsets, mcpOption, dynamicToolsets, toolsetGroup: sharedToolsetGroup, }) {
15
21
  const server = wrapServerWithToolRegistry(new McpServer({
16
22
  name: 'backlog',
17
23
  title: useFields ? 'backlog (field selection enabled)' : 'backlog',
18
24
  version,
19
- }));
20
- const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets);
25
+ }, dynamicToolsets ? undefined : { cacheHints: TOOL_LIST_CACHE_HINT }));
26
+ const toolsetGroup = sharedToolsetGroup ??
27
+ buildToolsetGroup(backlog, transHelper, enabledToolsets);
21
28
  registerTools(server, toolsetGroup, mcpOption);
22
- registerDynamicTools(server, organizationTools(clientRegistry, transHelper), mcpOption.prefix);
29
+ // `list_organizations` only has something to report when more than one space
30
+ // is configured; the `organization` parameter its description points at is
31
+ // published under the same condition.
32
+ if (mcpOption.useOrganization) {
33
+ registerDynamicTools(server, organizationTools(clientRegistry, transHelper), mcpOption.prefix);
34
+ }
23
35
  if (dynamicToolsets) {
24
36
  const registrar = createToolRegistrar(server, toolsetGroup, mcpOption);
25
37
  const dynamicToolsetGroup = dynamicTools(registrar, transHelper, toolsetGroup);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resolves the schema strings every tool definition passes through `t()`: tool
3
+ * descriptions and parameter descriptions. These are read by the model when it
4
+ * picks a tool and fills in arguments; they never reach the end user.
5
+ *
6
+ * This module intentionally imports nothing. Every tool in `src/tools/` depends on
7
+ * it, so anything imported here is reachable from the tool layer — and the tool
8
+ * layer is meant to run on non-Node runtimes too. Discovering and reading the
9
+ * override file needs a filesystem and a home directory, so that part lives in
10
+ * `loadTranslationOverrides` and the CLI passes the result in.
11
+ */
12
+ export interface TranslationHelper {
13
+ t: (key: string, fallback: string) => string;
14
+ dump: () => Record<string, string>;
15
+ }
16
+ export declare function createTranslationHelper(overrides?: Record<string, string>): TranslationHelper;
@@ -1,13 +1,5 @@
1
- import { cosmiconfigSync } from 'cosmiconfig';
2
- import os from 'os';
3
- export function createTranslationHelper(options) {
1
+ export function createTranslationHelper(overrides = {}) {
4
2
  const usedKeys = {};
5
- const configName = options?.configName ?? 'backlog-mcp-server';
6
- // Load config file
7
- const explorer = cosmiconfigSync(configName);
8
- const searchPath = options?.searchDir ?? os.homedir();
9
- const configResult = explorer.search(searchPath);
10
- const config = configResult?.config || {};
11
3
  function toEnvKey(key) {
12
4
  return `BACKLOG_MCP_${key}`;
13
5
  }
@@ -16,8 +8,12 @@ export function createTranslationHelper(options) {
16
8
  if (usedKeys[upperKey]) {
17
9
  return usedKeys[upperKey];
18
10
  }
19
- // Priority:ENV config fallback
20
- const value = process.env[toEnvKey(upperKey)] || config[upperKey] || fallback;
11
+ // Runtimes without a Node compatibility layer have no `process`, and a
12
+ // partial shim can have `process` without `env`.
13
+ const env = typeof process === 'undefined' ? undefined : process.env;
14
+ const fromEnv = env?.[toEnvKey(upperKey)];
15
+ // Priority:ENV → overrides → fallback
16
+ const value = fromEnv || overrides[upperKey] || fallback;
21
17
  usedKeys[upperKey] = value;
22
18
  return value;
23
19
  }
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { ErrorLike } from '../../types/result.js';
3
+ import { ToolDefinition } from '../../types/tool.js';
4
+ export interface ComposeOptions {
5
+ useFields: boolean;
6
+ errorHandler?: (err: unknown) => ErrorLike;
7
+ maxTokens: number;
8
+ /**
9
+ * Whether to advertise `organization`. Defaults to false: with a single
10
+ * Backlog space the parameter has exactly one legal value, and repeating it
11
+ * across every tool costs the client ~8 KB of schema for nothing.
12
+ */
13
+ useOrganization?: boolean;
14
+ }
15
+ type ComposedInput = {
16
+ fields?: string;
17
+ organization?: string;
18
+ } & Record<string, unknown>;
19
+ /**
20
+ * Builds the schema and handler a tool is registered with.
21
+ *
22
+ * The returned schema is a fresh object: the tool definition is never mutated.
23
+ * That matters under the stateless HTTP model, where one toolset group is shared
24
+ * by every per-request server — an in-place extension would be re-applied on
25
+ * every request, against a definition other requests are reading concurrently.
26
+ */
27
+ export declare function composeToolHandler(tool: ToolDefinition<any, any>, options: ComposeOptions): {
28
+ schema: z.ZodObject<any, z.core.$strip>;
29
+ handler: (input: ComposedInput, extra: import("@modelcontextprotocol/server").ServerContext) => Promise<import("@modelcontextprotocol/server").CallToolResult>;
30
+ };
31
+ export {};