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.
- package/README.ja.md +24 -16
- package/README.md +26 -16
- package/build/auth/backlogAuthContext.d.ts +2 -0
- package/build/auth/backlogOAuthClient.d.ts +10 -0
- package/build/auth/backlogOAuthClient.js +3 -3
- package/build/auth/backlogOAuthConfig.d.ts +9 -0
- package/build/auth/bearerAuthMiddleware.d.ts +4 -0
- package/build/auth/oauthRoutes.d.ts +4 -0
- package/build/auth/oauthRoutes.js +12 -5
- package/build/auth/tokenStore.d.ts +62 -0
- package/build/backlog/backlogErrorHandler.d.ts +2 -0
- package/build/backlog/customFields.d.ts +29 -0
- package/build/backlog/parseBacklogAPIError.d.ts +8 -0
- package/build/createBacklogMcpServer.d.ts +29 -0
- package/build/createBacklogMcpServer.js +18 -6
- package/build/createTranslationHelper.d.ts +16 -0
- package/build/createTranslationHelper.js +7 -11
- package/build/handlers/builders/composeToolHandler.d.ts +31 -0
- package/build/handlers/builders/composeToolHandler.js +24 -9
- package/build/handlers/transformers/wrapWithErrorHandling.d.ts +2 -0
- package/build/handlers/transformers/wrapWithFieldPicking.d.ts +4 -0
- package/build/handlers/transformers/wrapWithOrganizationContext.d.ts +3 -0
- package/build/handlers/transformers/wrapWithTokenLimit.d.ts +2 -0
- package/build/handlers/transformers/wrapWithToolResult.d.ts +6 -0
- package/build/httpMcpServer.d.ts +22 -0
- package/build/httpMcpServer.js +46 -100
- package/build/index.d.ts +2 -0
- package/build/index.js +33 -10
- package/build/lib.d.ts +25 -0
- package/build/lib.js +20 -0
- package/build/loadTranslationOverrides.d.ts +17 -0
- package/build/loadTranslationOverrides.js +24 -0
- package/build/registerTools.d.ts +5 -0
- package/build/registerTools.js +7 -6
- package/build/tools/addDocument.d.ts +15 -0
- package/build/tools/addIssue.d.ts +30 -0
- package/build/tools/addIssueComment.d.ts +14 -0
- package/build/tools/addProject.d.ts +18 -0
- package/build/tools/addPullRequest.d.ts +20 -0
- package/build/tools/addPullRequestComment.d.ts +16 -0
- package/build/tools/addRelatedIssue.d.ts +12 -0
- package/build/tools/addVersionMilestone.d.ts +15 -0
- package/build/tools/addWatching.d.ts +11 -0
- package/build/tools/addWiki.d.ts +13 -0
- package/build/tools/countIssues.d.ts +48 -0
- package/build/tools/deleteIssue.d.ts +11 -0
- package/build/tools/deleteProject.d.ts +11 -0
- package/build/tools/deleteVersion.d.ts +12 -0
- package/build/tools/deleteWatching.d.ts +10 -0
- package/build/tools/dynamicTools/organizations.d.ts +6 -0
- package/build/tools/dynamicTools/toolsets.d.ts +15 -0
- package/build/tools/getCategories.d.ts +11 -0
- package/build/tools/getCustomFields.d.ts +12 -0
- package/build/tools/getDocument.d.ts +10 -0
- package/build/tools/getDocumentTree.d.ts +10 -0
- package/build/tools/getDocuments.d.ts +11 -0
- package/build/tools/getGitRepositories.d.ts +11 -0
- package/build/tools/getGitRepository.d.ts +13 -0
- package/build/tools/getIssue.d.ts +11 -0
- package/build/tools/getIssueComments.d.ts +18 -0
- package/build/tools/getIssueTypes.d.ts +11 -0
- package/build/tools/getIssues.d.ts +75 -0
- package/build/tools/getMyself.d.ts +7 -0
- package/build/tools/getNotifications.d.ts +16 -0
- package/build/tools/getNotificationsCount.d.ts +11 -0
- package/build/tools/getPriorities.d.ts +7 -0
- package/build/tools/getProject.d.ts +11 -0
- package/build/tools/getProjectList.d.ts +11 -0
- package/build/tools/getProjectUsers.d.ts +11 -0
- package/build/tools/getPullRequest.d.ts +14 -0
- package/build/tools/getPullRequestComments.d.ts +21 -0
- package/build/tools/getPullRequests.d.ts +19 -0
- package/build/tools/getPullRequestsCount.d.ts +17 -0
- package/build/tools/getRelatedIssues.d.ts +11 -0
- package/build/tools/getResolutions.d.ts +7 -0
- package/build/tools/getSpace.d.ts +7 -0
- package/build/tools/getSpaceActivities.d.ts +45 -0
- package/build/tools/getUserRecentUpdates.d.ts +46 -0
- package/build/tools/getUserStarsCount.d.ts +12 -0
- package/build/tools/getUsers.d.ts +7 -0
- package/build/tools/getVersionMilestoneList.d.ts +11 -0
- package/build/tools/getWatchingListCount.d.ts +10 -0
- package/build/tools/getWatchingListItems.d.ts +10 -0
- package/build/tools/getWiki.d.ts +10 -0
- package/build/tools/getWikiPages.d.ts +12 -0
- package/build/tools/getWikisCount.d.ts +11 -0
- package/build/tools/markNotificationAsRead.d.ts +13 -0
- package/build/tools/markWatchingAsRead.d.ts +13 -0
- package/build/tools/removeRelatedIssue.d.ts +12 -0
- package/build/tools/resetUnreadNotificationCount.d.ts +7 -0
- package/build/tools/shared/customFieldFiltersSchema.d.ts +21 -0
- package/build/tools/tools.d.ts +4 -0
- package/build/tools/updateIssue.d.ts +34 -0
- package/build/tools/updateIssue.js +31 -4
- package/build/tools/updateIssueComment.d.ts +13 -0
- package/build/tools/updateProject.d.ts +21 -0
- package/build/tools/updatePullRequest.d.ts +20 -0
- package/build/tools/updatePullRequestComment.d.ts +16 -0
- package/build/tools/updateVersionMilestone.d.ts +17 -0
- package/build/tools/updateWatching.d.ts +11 -0
- package/build/tools/updateWiki.d.ts +13 -0
- package/build/types/mcp.d.ts +10 -0
- package/build/types/result.d.ts +10 -0
- package/build/types/tool.d.ts +24 -0
- package/build/types/toolsets.d.ts +16 -0
- package/build/types/zod/backlogOutputDefinition.d.ts +3334 -0
- package/build/types/zod/backlogOutputDefinition.js +3 -16
- package/build/utils/backlogClientRegistry.d.ts +28 -0
- package/build/utils/backlogClientRegistry.js +4 -0
- package/build/utils/backlogOrganizationContext.d.ts +2 -0
- package/build/utils/generateFieldsDescription.d.ts +5 -0
- package/build/utils/generateFieldsDescription.js +3 -0
- package/build/utils/logger.d.ts +2 -0
- package/build/utils/logger.js +27 -14
- package/build/utils/resolveIdOrKey.d.ts +18 -0
- package/build/utils/runToolSafely.d.ts +6 -0
- package/build/utils/tokenCounter.d.ts +1 -0
- package/build/utils/toolRegistrar.d.ts +5 -0
- package/build/utils/toolsetUtils.d.ts +6 -0
- package/build/utils/toolsetUtils.js +0 -20
- package/build/utils/wrapServerWithToolRegistry.d.ts +6 -0
- package/build/utils/wrapServerWithToolRegistry.js +1 -1
- 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`
|
|
160
|
-
| `BACKLOG_OAUTH_CLIENT_SECRET` | Backlogアプリケーションの OAuth Client Secret
|
|
161
|
-
| `MCP_SERVER_BASE_URL`
|
|
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`
|
|
180
|
-
| `GET /.well-known/oauth-protected-resource/mcp` | OAuthリソースメタデータ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728))
|
|
181
|
-
| `POST /register`
|
|
182
|
-
| `GET /authorize`
|
|
183
|
-
| `GET /callback`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
153
|
-
- **
|
|
154
|
-
- **
|
|
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
|
|
159
|
-
|
|
|
160
|
-
| `MCP_TRANSPORT`
|
|
161
|
-
| `MCP_HTTP_HOST`
|
|
162
|
-
| `MCP_HTTP_PORT`
|
|
163
|
-
| `MCP_HTTP_PATH`
|
|
164
|
-
| `MCP_HTTP_JSON_RESPONSE`
|
|
165
|
-
| `MCP_HTTP_ALLOWED_HOSTS`
|
|
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
|
|
648
|
-
- `--http-allowed-hosts`: Comma-separated allowed `Host`
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
|
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
|
|
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
|
|
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;
|
|
@@ -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
|
-
|
|
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 =
|
|
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,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/
|
|
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
|
|
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 =
|
|
25
|
+
}, dynamicToolsets ? undefined : { cacheHints: TOOL_LIST_CACHE_HINT }));
|
|
26
|
+
const toolsetGroup = sharedToolsetGroup ??
|
|
27
|
+
buildToolsetGroup(backlog, transHelper, enabledToolsets);
|
|
21
28
|
registerTools(server, toolsetGroup, mcpOption);
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
20
|
-
|
|
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 {};
|