backlog-mcp-server 0.16.0 → 0.18.0

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 (157) hide show
  1. package/README.ja.md +15 -32
  2. package/README.md +20 -32
  3. package/build/auth/oauthRoutes.js +53 -1
  4. package/build/createBacklogMcpServer.d.ts +4 -6
  5. package/build/createBacklogMcpServer.js +4 -12
  6. package/build/handlers/builders/composeToolHandler.d.ts +1 -1
  7. package/build/handlers/builders/composeToolHandler.js +15 -9
  8. package/build/handlers/transformers/wrapWithFieldPicking.d.ts +14 -1
  9. package/build/handlers/transformers/wrapWithFieldPicking.js +30 -36
  10. package/build/index.js +30 -16
  11. package/build/lib.d.ts +1 -1
  12. package/build/loadDescriptionOverrides.d.ts +0 -13
  13. package/build/loadDescriptionOverrides.js +73 -11
  14. package/build/reportUnknownOverrideKeys.d.ts +22 -0
  15. package/build/reportUnknownOverrideKeys.js +42 -0
  16. package/build/tools/addDocument.d.ts +2 -2
  17. package/build/tools/addDocument.js +17 -2
  18. package/build/tools/addIssue.d.ts +2 -2
  19. package/build/tools/addIssue.js +32 -2
  20. package/build/tools/addIssueComment.d.ts +2 -2
  21. package/build/tools/addIssueComment.js +14 -2
  22. package/build/tools/addProject.d.ts +2 -2
  23. package/build/tools/addProject.js +22 -2
  24. package/build/tools/addPullRequest.d.ts +2 -2
  25. package/build/tools/addPullRequest.js +26 -2
  26. package/build/tools/addPullRequestComment.d.ts +2 -2
  27. package/build/tools/addPullRequestComment.js +12 -2
  28. package/build/tools/addRelatedIssue.d.ts +2 -2
  29. package/build/tools/addRelatedIssue.js +33 -2
  30. package/build/tools/addVersionMilestone.d.ts +2 -2
  31. package/build/tools/addVersionMilestone.js +12 -2
  32. package/build/tools/addWatching.d.ts +2 -2
  33. package/build/tools/addWatching.js +12 -2
  34. package/build/tools/addWiki.d.ts +2 -2
  35. package/build/tools/addWiki.js +16 -2
  36. package/build/tools/countIssues.d.ts +2 -2
  37. package/build/tools/countIssues.js +3 -2
  38. package/build/tools/deleteIssue.d.ts +2 -2
  39. package/build/tools/deleteIssue.js +32 -2
  40. package/build/tools/deleteProject.d.ts +2 -2
  41. package/build/tools/deleteProject.js +22 -2
  42. package/build/tools/deleteVersion.d.ts +2 -2
  43. package/build/tools/deleteVersion.js +12 -2
  44. package/build/tools/deleteWatching.d.ts +2 -2
  45. package/build/tools/deleteWatching.js +12 -2
  46. package/build/tools/getCategories.d.ts +2 -2
  47. package/build/tools/getCategories.js +8 -2
  48. package/build/tools/getCustomFields.d.ts +2 -2
  49. package/build/tools/getCustomFields.js +22 -2
  50. package/build/tools/getDocument.d.ts +2 -2
  51. package/build/tools/getDocument.js +17 -2
  52. package/build/tools/getDocumentTree.d.ts +2 -2
  53. package/build/tools/getDocumentTree.js +7 -2
  54. package/build/tools/getDocuments.d.ts +2 -2
  55. package/build/tools/getDocuments.js +17 -2
  56. package/build/tools/getGitRepositories.d.ts +2 -2
  57. package/build/tools/getGitRepositories.js +17 -2
  58. package/build/tools/getGitRepository.d.ts +2 -2
  59. package/build/tools/getGitRepository.js +17 -2
  60. package/build/tools/getIssue.d.ts +2 -2
  61. package/build/tools/getIssue.js +32 -2
  62. package/build/tools/getIssueComments.d.ts +2 -2
  63. package/build/tools/getIssueComments.js +14 -2
  64. package/build/tools/getIssueTypes.d.ts +2 -2
  65. package/build/tools/getIssueTypes.js +11 -2
  66. package/build/tools/getIssues.d.ts +10 -10
  67. package/build/tools/getIssues.js +32 -2
  68. package/build/tools/getMyself.d.ts +2 -2
  69. package/build/tools/getMyself.js +11 -2
  70. package/build/tools/getNotifications.d.ts +2 -2
  71. package/build/tools/getNotifications.js +15 -2
  72. package/build/tools/getNotificationsCount.d.ts +2 -2
  73. package/build/tools/getNotificationsCount.js +5 -2
  74. package/build/tools/getPriorities.d.ts +2 -2
  75. package/build/tools/getPriorities.js +3 -2
  76. package/build/tools/getProject.d.ts +2 -2
  77. package/build/tools/getProject.js +22 -2
  78. package/build/tools/getProjectList.d.ts +2 -2
  79. package/build/tools/getProjectList.js +22 -2
  80. package/build/tools/getProjectUsers.d.ts +2 -2
  81. package/build/tools/getProjectUsers.js +11 -2
  82. package/build/tools/getPullRequest.d.ts +2 -2
  83. package/build/tools/getPullRequest.js +26 -2
  84. package/build/tools/getPullRequestComments.d.ts +2 -2
  85. package/build/tools/getPullRequestComments.js +12 -2
  86. package/build/tools/getPullRequests.d.ts +2 -2
  87. package/build/tools/getPullRequests.js +26 -2
  88. package/build/tools/getPullRequestsCount.d.ts +2 -2
  89. package/build/tools/getPullRequestsCount.js +5 -2
  90. package/build/tools/getRelatedIssues.d.ts +2 -2
  91. package/build/tools/getRelatedIssues.js +33 -2
  92. package/build/tools/getResolutions.d.ts +2 -2
  93. package/build/tools/getResolutions.js +3 -2
  94. package/build/tools/getSpace.d.ts +2 -2
  95. package/build/tools/getSpace.js +13 -2
  96. package/build/tools/getSpaceActivities.d.ts +2 -2
  97. package/build/tools/getSpaceActivities.js +12 -2
  98. package/build/tools/getUserRecentUpdates.d.ts +2 -2
  99. package/build/tools/getUserRecentUpdates.js +12 -2
  100. package/build/tools/getUserStarsCount.d.ts +2 -2
  101. package/build/tools/getUserStarsCount.js +3 -2
  102. package/build/tools/getUsers.d.ts +2 -2
  103. package/build/tools/getUsers.js +11 -2
  104. package/build/tools/getVersionMilestoneList.d.ts +2 -2
  105. package/build/tools/getVersionMilestoneList.js +12 -2
  106. package/build/tools/getWatchingListCount.d.ts +2 -2
  107. package/build/tools/getWatchingListCount.js +5 -2
  108. package/build/tools/getWatchingListItems.d.ts +2 -2
  109. package/build/tools/getWatchingListItems.js +12 -2
  110. package/build/tools/getWiki.d.ts +2 -2
  111. package/build/tools/getWiki.js +16 -2
  112. package/build/tools/getWikiPages.d.ts +2 -2
  113. package/build/tools/getWikiPages.js +12 -2
  114. package/build/tools/getWikisCount.d.ts +2 -2
  115. package/build/tools/getWikisCount.js +3 -2
  116. package/build/tools/markNotificationAsRead.d.ts +6 -1
  117. package/build/tools/markNotificationAsRead.js +6 -1
  118. package/build/tools/markWatchingAsRead.d.ts +6 -1
  119. package/build/tools/markWatchingAsRead.js +6 -1
  120. package/build/tools/removeRelatedIssue.d.ts +2 -2
  121. package/build/tools/removeRelatedIssue.js +33 -2
  122. package/build/tools/resetUnreadNotificationCount.d.ts +2 -2
  123. package/build/tools/resetUnreadNotificationCount.js +5 -2
  124. package/build/tools/updateIssue.d.ts +2 -2
  125. package/build/tools/updateIssue.js +32 -2
  126. package/build/tools/updateIssueComment.d.ts +2 -2
  127. package/build/tools/updateIssueComment.js +14 -2
  128. package/build/tools/updateProject.d.ts +2 -2
  129. package/build/tools/updateProject.js +22 -2
  130. package/build/tools/updatePullRequest.d.ts +2 -2
  131. package/build/tools/updatePullRequest.js +26 -2
  132. package/build/tools/updatePullRequestComment.d.ts +2 -2
  133. package/build/tools/updatePullRequestComment.js +12 -2
  134. package/build/tools/updateVersionMilestone.d.ts +2 -2
  135. package/build/tools/updateVersionMilestone.js +12 -2
  136. package/build/tools/updateWatching.d.ts +2 -2
  137. package/build/tools/updateWatching.js +12 -2
  138. package/build/tools/updateWiki.d.ts +2 -2
  139. package/build/tools/updateWiki.js +16 -2
  140. package/build/types/outputFields.d.ts +26 -0
  141. package/build/types/outputFields.js +3 -0
  142. package/build/types/tool.d.ts +21 -8
  143. package/build/types/zod/activityType.d.ts +38 -0
  144. package/build/types/zod/activityType.js +38 -0
  145. package/build/utils/fieldSelection.d.ts +22 -0
  146. package/build/utils/fieldSelection.js +40 -0
  147. package/build/utils/toolsetUtils.d.ts +1 -3
  148. package/build/utils/toolsetUtils.js +0 -12
  149. package/package.json +4 -4
  150. package/build/tools/dynamicTools/toolsets.d.ts +0 -15
  151. package/build/tools/dynamicTools/toolsets.js +0 -103
  152. package/build/types/zod/backlogOutputDefinition.d.ts +0 -3334
  153. package/build/types/zod/backlogOutputDefinition.js +0 -483
  154. package/build/utils/generateFieldsDescription.d.ts +0 -5
  155. package/build/utils/generateFieldsDescription.js +0 -50
  156. package/build/utils/toolRegistrar.d.ts +0 -5
  157. package/build/utils/toolRegistrar.js +0 -12
package/README.ja.md CHANGED
@@ -16,7 +16,7 @@ Backlog API とやり取りするための Model Context Protocol(MCP)サー
16
16
  - Wikiページサポート
17
17
  - Gitリポジトリとプルリクエストツール
18
18
  - 通知ツール
19
- - 最適化されたレスポンスのためのGraphQLスタイルのフィールド選択
19
+ - 最適化されたレスポンスのためのフィールド選択
20
20
  - 大規模なレスポンスに対するトークン制限
21
21
 
22
22
  ## 利用開始
@@ -188,6 +188,14 @@ OAuth有効時、サーバーは以下のOAuthエンドポイントを自動的
188
188
 
189
189
  MCP認可仕様に対応するMCPクライアントは、これらのエンドポイントを自動的に使用します。
190
190
 
191
+ `POST /register` は登録できる redirect URI を制限します。ループバック URI
192
+ (`http://localhost`、`http://127.0.0.1`、`http://[::1]`)はユーザーのマシン上で動く
193
+ アプリが認可コードを受け取るための手段で、`"application_type": "native"` を宣言した
194
+ クライアント、または宣言がない場合は redirect URI が**すべて**ループバックである
195
+ クライアントから受け付けます。`"application_type": "web"` を宣言したクライアントや、
196
+ 宣言なしでリモートの `https:` URI とループバック URI を混在させた登録は
197
+ `invalid_client_metadata` で拒否されます。
198
+
191
199
  > **制約事項:**
192
200
  >
193
201
  > - OAuthモードは現在、単一のBacklog組織のみをサポートしています。複数組織設定との併用はできません。
@@ -233,26 +241,6 @@ ENABLE_TOOLSETS="space,project,issue"
233
241
 
234
242
  > 🧩 ヒント: `project` ツールセットは、他の多くのツールがエントリポイントとしてプロジェクトデータに依存しているため、強く推奨されます。
235
243
 
236
- ### 動的なツールセット検出(実験的)
237
-
238
- MCPサーバーをAIエージェントと共に使用している場合、実行時にツールセットの動的な検出を有効にすることができます:
239
-
240
- CLI経由での有効化:
241
-
242
- ```
243
- --dynamic-toolsets
244
- ```
245
-
246
- または環境変数経由:
247
-
248
- ```
249
- -e DYNAMIC_TOOLSETS=1 \
250
- ```
251
-
252
- 動的ツールセットを有効にすると、LLMはツールインターフェースを介してオンデマンドでツールセットを一覧表示およびアクティブ化できるようになります。
253
-
254
- > **HTTPでのスコープ:** MCP `2026-07-28` にはプロトコルセッションがないため、アクティブ化したツールセットはクライアント単位ではなく**サーバープロセス単位**で保持されます。HTTPトランスポートでは接続中の全クライアントが1つのツールセット状態を共有し、プロセス再起動でリセットされます。共有されるのはツールの*可視性*のみで、認可は共有されません(各呼び出しは呼び出し元自身の認証情報で認証されます)。
255
-
256
244
  ## 利用可能なツール
257
245
 
258
246
  以下のような Backlog 機能に対応するツールを提供しています:
@@ -437,7 +425,7 @@ PREFIX="backlog_"
437
425
 
438
426
  ### レスポンスの最適化とトークン制限
439
427
 
440
- #### フィールド選択(GraphQLスタイル)
428
+ #### フィールド選択
441
429
 
442
430
  ```
443
431
  --optimize-response
@@ -449,17 +437,13 @@ PREFIX="backlog_"
449
437
  OPTIMIZE_RESPONSE=1
450
438
  ```
451
439
 
452
- 次に、特定のフィールドのみを要求します:
440
+ **一覧を返すツール**に `fields` パラメータ(省略可)が追加されます。値はそのツールの結果のトップレベルのフィールド名のリストで、enum として公開されるため、そのツールに存在しない名前は無視されずに拒否されます。単体のレコードを返すツールには追加されません(パラメータは毎セッション費用がかかる一方、1 件のレコードから削れる量はごくわずかなためです)。
453
441
 
454
442
  ```
455
- get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
443
+ get_project(projectIdOrKey: "PROJECT-KEY", fields: ["name", "key", "description"])
456
444
  ```
457
445
 
458
- AIはフィールド選択を使用してレスポンスを最適化します:
459
-
460
- ```
461
- get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
462
- ```
446
+ `fields` を省略すると結果全体が返ります。選択は 1 階層のみで、オブジェクトや配列のフィールドを指定した場合はそのまま丸ごと返ります。
463
447
 
464
448
  利点:
465
449
 
@@ -518,8 +502,7 @@ MAX_TOKENS=10000
518
502
  "MAX_TOKENS": "10000",
519
503
  "OPTIMIZE_RESPONSE": "1",
520
504
  "PREFIX": "backlog_",
521
- "ENABLE_TOOLSETS": "space,project,issue",
522
- "ENABLE_DYNAMIC_TOOLSETS": "1"
505
+ "ENABLE_TOOLSETS": "space,project,issue"
523
506
  }
524
507
  }
525
508
  }
@@ -546,7 +529,7 @@ pnpm test
546
529
  サーバーはいくつかのコマンドラインオプションをサポートしています:
547
530
 
548
531
  - `--export-descriptions`: ツール一覧の構築時に解決される説明キーと値をエクスポート。旧名は `--export-translations` で、非推奨エイリアスとして当面動作しますが、将来のリリースで削除されます
549
- - `--optimize-response`: GraphQLスタイルのフィールド選択を有効にする
532
+ - `--optimize-response`: 各ツールに、返す結果フィールドを選ぶ `fields` パラメータを追加する
550
533
  - `--max-tokens=NUMBER`: レスポンスの最大トークン制限を設定
551
534
  - `--prefix=STRING`: すべてのツール名に付加するオプションの文字列プレフィックス(デフォルト:"")
552
535
  - `--enable-toolsets <toolsets...>`: 有効にするツールセットを指定します(カンマ区切りまたは複数の引数)。デフォルトは "all" です。
package/README.md CHANGED
@@ -17,7 +17,7 @@ A Model Context Protocol (MCP) server for interacting with the Backlog API. This
17
17
  - Wiki page support
18
18
  - Git repository and pull request tools
19
19
  - Notification tools
20
- - GraphQL-style field selection for optimized responses
20
+ - Field selection for optimized responses
21
21
  - Token limiting for large responses
22
22
 
23
23
  ## Getting Started
@@ -215,6 +215,14 @@ The server automatically exposes the following OAuth endpoints when OAuth is ena
215
215
 
216
216
  MCP clients that support the MCP authorization specification will use these endpoints automatically.
217
217
 
218
+ `POST /register` restricts which redirect URIs a client may register. A loopback
219
+ URI (`http://localhost`, `http://127.0.0.1`, `http://[::1]`) is how an app running
220
+ on the user's machine receives the authorization code, and is accepted from a
221
+ client that declares `"application_type": "native"` — or, when the field is
222
+ absent, from one whose redirect URIs are _all_ loopback. A client declaring
223
+ `"application_type": "web"`, or mixing a remote `https:` URI with a loopback one
224
+ without declaring itself, is rejected with `invalid_client_metadata`.
225
+
218
226
  > **Limitations:**
219
227
  >
220
228
  > - OAuth mode currently supports a single Backlog organization. It is not compatible with the multi-organization configuration.
@@ -260,26 +268,6 @@ Using selective toolsets can be helpful if the toolset list is too large for you
260
268
 
261
269
  > 🧩 Tip: `project` toolset is highly recommended, as many other tools rely on project data as an entry point.
262
270
 
263
- ### Dynamic Toolset Discovery (Experimental)
264
-
265
- If you're using the MCP server with AI agents, you can enable dynamic discovery of toolsets at runtime:
266
-
267
- Enabling via CLI:
268
-
269
- ```
270
- --dynamic-toolsets
271
- ```
272
-
273
- Or via environment variable::
274
-
275
- ```
276
- -e DYNAMIC_TOOLSETS=1 \
277
- ```
278
-
279
- With dynamic toolsets enabled, the LLM will be able to list and activate toolsets on demand via tool interface.
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
-
283
271
  ## Available Tools
284
272
 
285
273
  ### Toolset: `space`
@@ -549,7 +537,7 @@ This is especially useful if you're using multiple MCP servers or tools in the s
549
537
 
550
538
  ### Response Optimization & Token Limits
551
539
 
552
- #### Field Selection (GraphQL-style)
540
+ #### Field Selection
553
541
 
554
542
  ```
555
543
  --optimize-response
@@ -561,17 +549,18 @@ Or environment variable:
561
549
  OPTIMIZE_RESPONSE=1
562
550
  ```
563
551
 
564
- Then, request only specific fields:
552
+ Tools that return a **list** then take an optional `fields` parameter: a list of
553
+ top-level field names from that tool's own result, published as an enum so a name
554
+ the tool does not have is rejected rather than ignored. Tools that return a single
555
+ record do not get it — the parameter costs schema on every session, and one record
556
+ has almost nothing to trim.
565
557
 
566
558
  ```
567
- get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
559
+ get_project(projectIdOrKey: "PROJECT-KEY", fields: ["name", "key", "description"])
568
560
  ```
569
561
 
570
- The AI will use field selection to optimize the response:
571
-
572
- ```
573
- get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
574
- ```
562
+ Omitting `fields` returns the whole result. Selection is one level deep: naming an
563
+ object or array field returns it whole.
575
564
 
576
565
  Benefits:
577
566
 
@@ -630,8 +619,7 @@ This section demonstrates advanced configuration using multiple environment vari
630
619
  "MAX_TOKENS": "10000",
631
620
  "OPTIMIZE_RESPONSE": "1",
632
621
  "PREFIX": "backlog_",
633
- "ENABLE_TOOLSETS": "space,project,issue",
634
- "ENABLE_DYNAMIC_TOOLSETS": "1"
622
+ "ENABLE_TOOLSETS": "space,project,issue"
635
623
  }
636
624
  }
637
625
  }
@@ -663,7 +651,7 @@ The server supports several command line options:
663
651
  - `--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.
664
652
  - `--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.
665
653
  - `--export-descriptions`: Export the description keys and values resolved when building the tool list. Was named `--export-translations`; that spelling still works as a deprecated alias and will be removed in a future release
666
- - `--optimize-response`: Enable GraphQL-style field selection
654
+ - `--optimize-response`: Add a `fields` parameter to each tool for selecting which result fields to return
667
655
  - `--max-tokens=NUMBER`: Set maximum token limit for responses
668
656
  - `--prefix=STRING`: Optional string prefix to prepend to all tool names (default: "")
669
657
  - `--enable-toolsets <toolsets...>`: Specify which toolsets to enable (comma-separated or multiple arguments). Defaults to "all".
@@ -15,17 +15,54 @@ function verifyPkce(codeVerifier, codeChallenge) {
15
15
  function oauthError(code, description) {
16
16
  return { error: code, error_description: description };
17
17
  }
18
+ function isLoopbackRedirectUri(uri) {
19
+ try {
20
+ const parsed = new URL(uri);
21
+ return (parsed.protocol === 'http:' && LOCALHOST_HOSTS.includes(parsed.hostname));
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
18
27
  function isValidRedirectUri(uri) {
19
28
  try {
20
29
  const parsed = new URL(uri);
21
30
  if (parsed.protocol === 'https:')
22
31
  return true;
23
- return (parsed.protocol === 'http:' && LOCALHOST_HOSTS.includes(parsed.hostname));
32
+ return isLoopbackRedirectUri(uri);
24
33
  }
25
34
  catch {
26
35
  return false;
27
36
  }
28
37
  }
38
+ /** RFC 7591 defines these two, and defaults to `web` when the field is absent. */
39
+ const APPLICATION_TYPES = ['web', 'native'];
40
+ /**
41
+ * Decides which application type a registration is held to.
42
+ *
43
+ * A loopback redirect URI is the standard way for an app on the user's machine
44
+ * to receive the authorization code (RFC 8252), and the wrong thing entirely for
45
+ * an app that has a domain of its own: nobody owns `http://localhost`, so
46
+ * whichever process is listening on that port receives the code.
47
+ *
48
+ * RFC 7591 defaults the field to `web`, which would reject every local MCP
49
+ * client that does not declare itself — and they do not, because nothing has
50
+ * ever checked. So an undeclared registration is judged by what it asks for: a
51
+ * set of redirect URIs that is entirely loopback can only be a native client,
52
+ * and is treated as one.
53
+ *
54
+ * The inference deliberately requires *all* of them. A registration mixing a
55
+ * remote https URI with a loopback one is the shape this check exists to stop:
56
+ * something that can serve a redirect on its own domain has no need to also
57
+ * collect codes on the user's machine. Mixing is only accepted from a client
58
+ * that declares `native` outright, which RFC 8252 does allow (a native app may
59
+ * claim an https URI as well as a loopback one).
60
+ */
61
+ function resolveApplicationType(declared, redirectUris) {
62
+ if (declared)
63
+ return declared;
64
+ return redirectUris.every(isLoopbackRedirectUri) ? 'native' : 'web';
65
+ }
29
66
  export function createOAuthRoutes(config, store, mcpPath) {
30
67
  const app = new Hono();
31
68
  const { serverBaseUrl } = config;
@@ -76,6 +113,21 @@ export function createOAuthRoutes(config, store, mcpPath) {
76
113
  return c.json(oauthError('invalid_client_metadata', `redirect_uri must use https or http://localhost: ${uri}`), 400);
77
114
  }
78
115
  }
116
+ // `null` counts as not declared. Serialisers that emit null for an absent
117
+ // optional field are common, and rejecting it would break clients this
118
+ // change is meant to leave alone.
119
+ const declaredApplicationType = body.application_type ?? undefined;
120
+ if (declaredApplicationType !== undefined &&
121
+ !APPLICATION_TYPES.includes(declaredApplicationType)) {
122
+ return c.json(oauthError('invalid_client_metadata', `Unsupported application_type: ${String(declaredApplicationType)}. Supported: ${APPLICATION_TYPES.join(', ')}`), 400);
123
+ }
124
+ const applicationType = resolveApplicationType(declaredApplicationType, redirectUris);
125
+ if (applicationType === 'web') {
126
+ const loopback = redirectUris.filter(isLoopbackRedirectUri);
127
+ if (loopback.length > 0) {
128
+ return c.json(oauthError('invalid_client_metadata', `application_type "web" may not use a loopback redirect_uri: ${loopback.join(', ')}. Declare application_type "native" if this client runs on the user's machine.`), 400);
129
+ }
130
+ }
79
131
  const authMethod = typeof body.token_endpoint_auth_method === 'string'
80
132
  ? body.token_endpoint_auth_method
81
133
  : 'client_secret_post';
@@ -12,13 +12,11 @@ export type CreateBacklogMcpServerConfig = {
12
12
  descriptionHelper: DescriptionHelper;
13
13
  enabledToolsets: string[];
14
14
  mcpOption: MCPOptions;
15
- dynamicToolsets: boolean;
16
15
  /**
17
16
  * 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.
17
+ * `enabledToolsets`. Callers that produce many servers from one factory pass a
18
+ * single shared group so the tool tree is built once rather than per request.
19
+ * Nothing mutates it.
22
20
  */
23
21
  toolsetGroup?: ToolsetGroup;
24
22
  };
@@ -26,4 +24,4 @@ export type CreateBacklogMcpServerConfig = {
26
24
  * Builds a fresh MCP server instance with all Backlog tools registered.
27
25
  * Used once per stdio connection; one instance per HTTP request for Streamable HTTP.
28
26
  */
29
- export declare function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, descriptionHelper, enabledToolsets, mcpOption, dynamicToolsets, toolsetGroup: sharedToolsetGroup, }: CreateBacklogMcpServerConfig): BacklogMCPServer;
27
+ export declare function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, descriptionHelper, enabledToolsets, mcpOption, toolsetGroup: sharedToolsetGroup, }: CreateBacklogMcpServerConfig): BacklogMCPServer;
@@ -3,13 +3,10 @@
3
3
  import { McpServer } from '@modelcontextprotocol/server';
4
4
  import { registerDynamicTools, registerTools } from './registerTools.js';
5
5
  import { organizationTools } from './tools/dynamicTools/organizations.js';
6
- import { dynamicTools } from './tools/dynamicTools/toolsets.js';
7
- import { createToolRegistrar } from './utils/toolRegistrar.js';
8
6
  import { buildToolsetGroup } from './utils/toolsetUtils.js';
9
7
  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.
8
+ // The tool list is fixed for the process lifetime: it only depends on CLI flags
9
+ // and environment, so clients may cache it.
13
10
  const TOOL_LIST_CACHE_HINT = {
14
11
  'tools/list': { ttlMs: 5 * 60 * 1000, cacheScope: 'public' },
15
12
  };
@@ -17,12 +14,12 @@ const TOOL_LIST_CACHE_HINT = {
17
14
  * Builds a fresh MCP server instance with all Backlog tools registered.
18
15
  * Used once per stdio connection; one instance per HTTP request for Streamable HTTP.
19
16
  */
20
- export function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, descriptionHelper, enabledToolsets, mcpOption, dynamicToolsets, toolsetGroup: sharedToolsetGroup, }) {
17
+ export function createBacklogMcpServer({ version, useFields, backlog, clientRegistry, descriptionHelper, enabledToolsets, mcpOption, toolsetGroup: sharedToolsetGroup, }) {
21
18
  const server = wrapServerWithToolRegistry(new McpServer({
22
19
  name: 'backlog',
23
20
  title: useFields ? 'backlog (field selection enabled)' : 'backlog',
24
21
  version,
25
- }, dynamicToolsets ? undefined : { cacheHints: TOOL_LIST_CACHE_HINT }));
22
+ }, { cacheHints: TOOL_LIST_CACHE_HINT }));
26
23
  const toolsetGroup = sharedToolsetGroup ??
27
24
  buildToolsetGroup(backlog, descriptionHelper, enabledToolsets);
28
25
  registerTools(server, toolsetGroup, mcpOption);
@@ -32,10 +29,5 @@ export function createBacklogMcpServer({ version, useFields, backlog, clientRegi
32
29
  if (mcpOption.useOrganization) {
33
30
  registerDynamicTools(server, organizationTools(clientRegistry, descriptionHelper), mcpOption.prefix);
34
31
  }
35
- if (dynamicToolsets) {
36
- const registrar = createToolRegistrar(server, toolsetGroup, mcpOption);
37
- const dynamicToolsetGroup = dynamicTools(registrar, descriptionHelper, toolsetGroup);
38
- registerDynamicTools(server, dynamicToolsetGroup, mcpOption.prefix);
39
- }
40
32
  return server;
41
33
  }
@@ -13,7 +13,7 @@ export interface ComposeOptions {
13
13
  useOrganization?: boolean;
14
14
  }
15
15
  type ComposedInput = {
16
- fields?: string;
16
+ fields?: string[];
17
17
  organization?: string;
18
18
  } & Record<string, unknown>;
19
19
  /**
@@ -5,7 +5,7 @@ import { wrapWithOrganizationContext } from '../transformers/wrapWithOrganizatio
5
5
  import { wrapWithTokenLimit } from '../transformers/wrapWithTokenLimit.js';
6
6
  import { wrapWithToolResult } from '../transformers/wrapWithToolResult.js';
7
7
  import { z } from 'zod';
8
- import { generateFieldsDescription } from '../../utils/generateFieldsDescription.js';
8
+ import { fieldSelectionSchema } from '../../utils/fieldSelection.js';
9
9
  /**
10
10
  * Builds the schema and handler a tool is registered with.
11
11
  *
@@ -16,20 +16,24 @@ import { generateFieldsDescription } from '../../utils/generateFieldsDescription
16
16
  */
17
17
  export function composeToolHandler(tool, options) {
18
18
  const { useFields, errorHandler, maxTokens, useOrganization = false, } = options;
19
- // Step 1: Add `fields` to schema if needed
20
- const fieldDesc = useFields
21
- ? generateFieldsDescription(tool.outputSchema, tool.importantFields ?? [], tool.name)
19
+ // Step 1: Add `fields` to schema if needed.
20
+ //
21
+ // Only on tools that return a list. That is where a response grows without
22
+ // bound and trimming it pays for the schema every client downloads; a tool
23
+ // returning one record saves a few hundred bytes at best.
24
+ const fields = useFields && tool.returnsList
25
+ ? fieldSelectionSchema(tool.outputFields, tool.importantFields ?? [])
22
26
  : undefined;
23
- const schema = extendSchema(tool.schema, fieldDesc, useOrganization);
27
+ const schema = extendSchema(tool.schema, fields, useOrganization);
24
28
  // Step 2: Compose
25
29
  const baseHandler = wrapWithErrorHandling(wrapWithOrganizationContext(tool.handler), errorHandler);
26
- const composed = useFields ? wrapWithFieldPicking(baseHandler) : baseHandler;
30
+ const composed = useFields && fields ? wrapWithFieldPicking(baseHandler) : baseHandler;
27
31
  return {
28
32
  schema,
29
33
  handler: wrapWithToolResult(wrapWithTokenLimit(composed, maxTokens)),
30
34
  };
31
35
  }
32
- function extendSchema(schema, desc, withOrganization = false) {
36
+ function extendSchema(schema, fields, withOrganization = false) {
33
37
  const extension = {};
34
38
  if (withOrganization) {
35
39
  extension.organization = z
@@ -37,8 +41,10 @@ function extendSchema(schema, desc, withOrganization = false) {
37
41
  .optional()
38
42
  .describe('Optional organization name. Use list_organizations to inspect available organizations.');
39
43
  }
40
- if (desc) {
41
- extension.fields = z.string().describe(desc);
44
+ // Optional, as `wrapWithFieldPicking` assumes: it returns the whole result
45
+ // when `fields` is absent.
46
+ if (fields) {
47
+ extension.fields = fields;
42
48
  }
43
49
  // zod v4 reworked the ZodObject shape generics, so `extend()`'s result no
44
50
  // longer overlaps the declared return type enough for a direct cast. The
@@ -1,4 +1,17 @@
1
1
  import { SafeResult } from '../../types/result.js';
2
+ /**
3
+ * Narrows a successful result to the fields the caller asked for.
4
+ *
5
+ * `fields` is a list of top-level names, validated against the tool's own enum
6
+ * before it reaches here, so an unknown name is rejected by the protocol rather
7
+ * than dropped in silence. A missing or empty list returns everything.
8
+ *
9
+ * One level deep on purpose. This used to accept a GraphQL selection set and
10
+ * parse it with the `graphql` package: ~1 MB for a single `parse` call, a grammar
11
+ * that accepted aliases, fragments, arguments and directives and then honoured
12
+ * none of them, and a descent into an array field that returned `{}` and lost the
13
+ * data outright.
14
+ */
2
15
  export declare function wrapWithFieldPicking<I extends {
3
- fields?: string;
16
+ fields?: string[];
4
17
  }, O>(fn: (input: I) => Promise<SafeResult<O>>): (input: I) => Promise<SafeResult<O>>;
@@ -1,55 +1,49 @@
1
- import { parse } from 'graphql';
2
1
  import { isErrorLike } from '../../types/result.js';
2
+ /**
3
+ * Narrows a successful result to the fields the caller asked for.
4
+ *
5
+ * `fields` is a list of top-level names, validated against the tool's own enum
6
+ * before it reaches here, so an unknown name is rejected by the protocol rather
7
+ * than dropped in silence. A missing or empty list returns everything.
8
+ *
9
+ * One level deep on purpose. This used to accept a GraphQL selection set and
10
+ * parse it with the `graphql` package: ~1 MB for a single `parse` call, a grammar
11
+ * that accepted aliases, fragments, arguments and directives and then honoured
12
+ * none of them, and a descent into an array field that returned `{}` and lost the
13
+ * data outright.
14
+ */
3
15
  export function wrapWithFieldPicking(fn) {
4
16
  return async (input) => {
5
17
  const { fields, ...rest } = input;
6
18
  const result = await fn(rest);
7
- if (!fields || isErrorLike(result)) {
19
+ if (!fields || fields.length === 0 || isErrorLike(result)) {
8
20
  return result;
9
21
  }
10
- const selectionSet = parseFieldsSelection(fields);
11
- const resultData = result.data;
12
- if (Array.isArray(resultData)) {
22
+ const data = result.data;
23
+ if (Array.isArray(data)) {
13
24
  return {
14
25
  kind: 'ok',
15
- data: resultData.map((item) => pickFieldsFromData(item, selectionSet)),
26
+ data: data.map((item) => pick(item, fields)),
16
27
  };
17
28
  }
18
- else if (typeof result === 'object' && result !== null) {
29
+ if (typeof data === 'object' && data !== null) {
19
30
  return {
20
31
  kind: 'ok',
21
- data: pickFieldsFromData(resultData, selectionSet),
32
+ data: pick(data, fields),
22
33
  };
23
34
  }
24
- else {
25
- return result;
26
- }
35
+ // A scalar has no fields to narrow.
36
+ return result;
27
37
  };
28
38
  }
29
- function parseFieldsSelection(fieldsString) {
30
- const query = `query Dummy ${fieldsString}`;
31
- const ast = parse(query);
32
- const opDef = ast.definitions[0];
33
- if (opDef.kind !== 'OperationDefinition' || !opDef.selectionSet) {
34
- throw new Error('Invalid GraphQL fields');
35
- }
36
- return opDef.selectionSet;
37
- }
38
- function pickFieldsFromData(data, selectionSet) {
39
- const result = {};
40
- for (const selection of selectionSet.selections) {
41
- if (selection.kind === 'Field') {
42
- const key = selection.name.value;
43
- if (data != null && key in data) {
44
- const value = data[key];
45
- if (selection.selectionSet && value != null) {
46
- result[key] = pickFieldsFromData(data[key], selection.selectionSet);
47
- }
48
- else {
49
- result[key] = data[key];
50
- }
51
- }
52
- }
39
+ function pick(value, fields) {
40
+ if (typeof value !== 'object' || value === null)
41
+ return value;
42
+ const source = value;
43
+ const picked = {};
44
+ for (const field of fields) {
45
+ if (field in source)
46
+ picked[field] = source[field];
53
47
  }
54
- return result;
48
+ return picked;
55
49
  }
package/build/index.js CHANGED
@@ -11,6 +11,7 @@ import { createDescriptionHelper } from './createDescriptionHelper.js';
11
11
  import { loadDescriptionOverrides } from './loadDescriptionOverrides.js';
12
12
  import { createBacklogMcpServer } from './createBacklogMcpServer.js';
13
13
  import { runHttpMcpServer } from './httpMcpServer.js';
14
+ import { reportUnknownOverrideKeys } from './reportUnknownOverrideKeys.js';
14
15
  import { createBacklogClientRegistry, createOAuthBacklogClientRegistry, } from './utils/backlogClientRegistry.js';
15
16
  import { logger } from './utils/logger.js';
16
17
  import { buildToolsetGroup } from './utils/toolsetUtils.js';
@@ -119,11 +120,6 @@ Available toolsets:
119
120
  - git: Tools for managing Git repositories and pull requests
120
121
  - notifications: Tools for managing user notifications`,
121
122
  default: env.get('ENABLE_TOOLSETS').default('all').asArray(','),
122
- })
123
- .option('dynamic-toolsets', {
124
- type: 'boolean',
125
- describe: 'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.',
126
- default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(),
127
123
  })
128
124
  .parseSync();
129
125
  // The alias resolves both spellings to the same argv key, so which one was typed
@@ -133,6 +129,22 @@ Available toolsets:
133
129
  if (hideBin(process.argv).some((arg) => arg.split('=')[0] === '--export-translations')) {
134
130
  process.stderr.write('--export-translations is deprecated and will be removed in a future release. Use --export-descriptions.\n');
135
131
  }
132
+ // Dynamic toolsets are gone. yargs ignores the unknown flag, so without this the
133
+ // server would start with a quietly different tool list: the flag used to drop
134
+ // `all` from the enabled toolsets, so a setup that passed only this one went from
135
+ // no toolsets plus three meta-tools to every toolset enabled.
136
+ //
137
+ // Only worth saying to someone who had it switched on. A setting left at `false`
138
+ // asked for what it now gets, so a notice claiming the tool list changed would be
139
+ // wrong.
140
+ const asksForDynamicToolsets = (value) => value !== undefined &&
141
+ !['', '0', 'false', 'no'].includes(value.toLowerCase());
142
+ const dynamicToolsetsFlag = hideBin(process.argv).find((arg) => arg.split('=')[0] === '--dynamic-toolsets');
143
+ if ((dynamicToolsetsFlag !== undefined &&
144
+ asksForDynamicToolsets(dynamicToolsetsFlag.split('=')[1] ?? 'true')) ||
145
+ asksForDynamicToolsets(process.env.ENABLE_DYNAMIC_TOOLSETS)) {
146
+ process.stderr.write('Dynamic toolsets have been removed, and --dynamic-toolsets / ENABLE_DYNAMIC_TOOLSETS no longer do anything. Every toolset is enabled unless you narrow it with --enable-toolsets or ENABLE_TOOLSETS.\n');
147
+ }
136
148
  const clientRegistry = oauthConfig
137
149
  ? createOAuthBacklogClientRegistry(oauthConfig.backlogDomain)
138
150
  : createBacklogClientRegistry();
@@ -144,24 +156,28 @@ if (tokenStore) {
144
156
  cleanupTimer.unref();
145
157
  }
146
158
  const useFields = argv.optimizeResponse;
147
- const descriptionHelper = createDescriptionHelper(loadDescriptionOverrides());
159
+ const descriptionOverrides = loadDescriptionOverrides();
160
+ const descriptionHelper = createDescriptionHelper(descriptionOverrides);
148
161
  const maxTokens = argv.maxTokens;
149
162
  const prefix = argv.prefix;
150
- const enabledToolsets = argv.dynamicToolsets
151
- ? argv.enableToolsets.filter((a) => a !== 'all')
152
- : argv.enableToolsets;
163
+ const enabledToolsets = argv.enableToolsets;
153
164
  const mcpOption = {
154
165
  useFields: useFields,
155
166
  maxTokens,
156
167
  prefix,
157
168
  useOrganization: clientRegistry.isMultiOrganization,
158
169
  };
159
- // Built once and shared by every server the factory produces. `enable_toolset`
160
- // mutates this group, and the stateless HTTP model discards its server after
161
- // each request so a per-server group would lose the enablement immediately.
162
- // Sharing it makes toolset state process-wide, which is the only scope left now
163
- // that the protocol has no sessions.
170
+ // Built once and shared by every server the factory produces. Nothing mutates
171
+ // it, so this is purely to avoid rebuilding the whole tool tree per request under
172
+ // the stateless HTTP model.
164
173
  const sharedToolsetGroup = buildToolsetGroup(backlog, descriptionHelper, enabledToolsets);
174
+ reportUnknownOverrideKeys({
175
+ overrides: descriptionOverrides,
176
+ version,
177
+ backlog,
178
+ clientRegistry,
179
+ mcpOption,
180
+ });
165
181
  // Factory: creates a fresh MCP server with all tools registered.
166
182
  // Used once per stdio connection; one fresh instance per HTTP request.
167
183
  const createServer = () => createBacklogMcpServer({
@@ -172,7 +188,6 @@ const createServer = () => createBacklogMcpServer({
172
188
  descriptionHelper,
173
189
  enabledToolsets,
174
190
  mcpOption,
175
- dynamicToolsets: argv.dynamicToolsets,
176
191
  toolsetGroup: sharedToolsetGroup,
177
192
  });
178
193
  if (argv.exportDescriptions) {
@@ -187,7 +202,6 @@ if (argv.exportDescriptions) {
187
202
  descriptionHelper,
188
203
  enabledToolsets: ['all'],
189
204
  mcpOption,
190
- dynamicToolsets: true,
191
205
  });
192
206
  const data = descriptionHelper.dump();
193
207
  // eslint-disable-next-line no-console
package/build/lib.d.ts CHANGED
@@ -20,6 +20,6 @@ export { buildToolSchema } from './types/tool.js';
20
20
  export { isErrorLike } from './types/result.js';
21
21
  export type { ComposeOptions } from './handlers/builders/composeToolHandler.js';
22
22
  export type { DescriptionHelper } from './createDescriptionHelper.js';
23
- export type { ToolDefinition, DynamicToolDefinition, ToolRegistrar, } from './types/tool.js';
23
+ export type { ToolDefinition, DynamicToolDefinition } from './types/tool.js';
24
24
  export type { Toolset, ToolsetGroup, DynamicToolset, DynamicToolsetGroup, } from './types/toolsets.js';
25
25
  export type { ErrorLike, SafeResult } from './types/result.js';
@@ -1,16 +1,3 @@
1
- /**
2
- * Reads description overrides from a `.backlog-mcp-serverrc` file (`.json`,
3
- * `.yaml` or `.yml`) in the user's home directory.
4
- *
5
- * Node-only, and kept separate from `createDescriptionHelper` for that reason:
6
- * cosmiconfig walks the filesystem and the default search path is the home
7
- * directory. The CLI calls this and hands the result to the helper.
8
- *
9
- * The file is user-authored, so its contents are unknown: anything that is not a
10
- * string is dropped here rather than passed on. Most overrides end up in a tool
11
- * description, and a number or an array there would produce an invalid
12
- * `tools/list` payload.
13
- */
14
1
  export declare function loadDescriptionOverrides(options?: {
15
2
  configName?: string;
16
3
  searchDir?: string;