backlog-mcp-server 0.17.0 → 0.18.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 +31 -32
- package/README.md +36 -32
- package/build/auth/oauthRoutes.js +53 -1
- package/build/backlog/customFields.js +12 -9
- package/build/createBacklogMcpServer.d.ts +4 -6
- package/build/createBacklogMcpServer.js +4 -12
- package/build/handlers/builders/composeToolHandler.d.ts +1 -1
- package/build/handlers/builders/composeToolHandler.js +15 -9
- package/build/handlers/transformers/wrapWithFieldPicking.d.ts +14 -1
- package/build/handlers/transformers/wrapWithFieldPicking.js +30 -36
- package/build/httpMcpServer.js +8 -1
- package/build/index.js +20 -15
- package/build/lib.d.ts +1 -1
- package/build/loadDescriptionOverrides.js +2 -2
- package/build/reportUnknownOverrideKeys.js +0 -1
- package/build/tools/addDocument.d.ts +3 -3
- package/build/tools/addDocument.js +18 -3
- package/build/tools/addIssue.d.ts +3 -3
- package/build/tools/addIssue.js +32 -2
- package/build/tools/addIssueComment.d.ts +3 -3
- package/build/tools/addIssueComment.js +14 -2
- package/build/tools/addProject.d.ts +3 -3
- package/build/tools/addProject.js +22 -2
- package/build/tools/addPullRequest.d.ts +3 -3
- package/build/tools/addPullRequest.js +26 -2
- package/build/tools/addPullRequestComment.d.ts +3 -3
- package/build/tools/addPullRequestComment.js +12 -2
- package/build/tools/addRelatedIssue.d.ts +3 -3
- package/build/tools/addRelatedIssue.js +33 -2
- package/build/tools/addVersionMilestone.d.ts +3 -3
- package/build/tools/addVersionMilestone.js +12 -2
- package/build/tools/addWatching.d.ts +3 -3
- package/build/tools/addWatching.js +12 -2
- package/build/tools/addWiki.d.ts +3 -3
- package/build/tools/addWiki.js +16 -2
- package/build/tools/countIssues.d.ts +3 -3
- package/build/tools/countIssues.js +3 -2
- package/build/tools/deleteIssue.d.ts +3 -3
- package/build/tools/deleteIssue.js +32 -2
- package/build/tools/deleteProject.d.ts +3 -3
- package/build/tools/deleteProject.js +22 -2
- package/build/tools/deleteVersion.d.ts +3 -3
- package/build/tools/deleteVersion.js +12 -2
- package/build/tools/deleteWatching.d.ts +3 -3
- package/build/tools/deleteWatching.js +12 -2
- package/build/tools/getCategories.d.ts +3 -3
- package/build/tools/getCategories.js +8 -2
- package/build/tools/getCustomFields.d.ts +3 -3
- package/build/tools/getCustomFields.js +22 -2
- package/build/tools/getDocument.d.ts +3 -3
- package/build/tools/getDocument.js +17 -2
- package/build/tools/getDocumentTree.d.ts +3 -3
- package/build/tools/getDocumentTree.js +7 -2
- package/build/tools/getDocuments.d.ts +3 -3
- package/build/tools/getDocuments.js +17 -2
- package/build/tools/getGitRepositories.d.ts +3 -3
- package/build/tools/getGitRepositories.js +17 -2
- package/build/tools/getGitRepository.d.ts +3 -3
- package/build/tools/getGitRepository.js +17 -2
- package/build/tools/getIssue.d.ts +3 -3
- package/build/tools/getIssue.js +32 -2
- package/build/tools/getIssueComments.d.ts +3 -3
- package/build/tools/getIssueComments.js +14 -2
- package/build/tools/getIssueTypes.d.ts +3 -3
- package/build/tools/getIssueTypes.js +11 -2
- package/build/tools/getIssues.d.ts +17 -17
- package/build/tools/getIssues.js +32 -2
- package/build/tools/getMyself.d.ts +3 -3
- package/build/tools/getMyself.js +11 -2
- package/build/tools/getNotifications.d.ts +3 -3
- package/build/tools/getNotifications.js +15 -2
- package/build/tools/getNotificationsCount.d.ts +3 -3
- package/build/tools/getNotificationsCount.js +5 -2
- package/build/tools/getPriorities.d.ts +3 -3
- package/build/tools/getPriorities.js +3 -2
- package/build/tools/getProject.d.ts +3 -3
- package/build/tools/getProject.js +22 -2
- package/build/tools/getProjectList.d.ts +3 -3
- package/build/tools/getProjectList.js +22 -2
- package/build/tools/getProjectUsers.d.ts +3 -3
- package/build/tools/getProjectUsers.js +11 -2
- package/build/tools/getPullRequest.d.ts +3 -3
- package/build/tools/getPullRequest.js +26 -2
- package/build/tools/getPullRequestComments.d.ts +3 -3
- package/build/tools/getPullRequestComments.js +12 -2
- package/build/tools/getPullRequests.d.ts +3 -3
- package/build/tools/getPullRequests.js +26 -2
- package/build/tools/getPullRequestsCount.d.ts +3 -3
- package/build/tools/getPullRequestsCount.js +5 -2
- package/build/tools/getRelatedIssues.d.ts +3 -3
- package/build/tools/getRelatedIssues.js +33 -2
- package/build/tools/getResolutions.d.ts +3 -3
- package/build/tools/getResolutions.js +3 -2
- package/build/tools/getSpace.d.ts +3 -3
- package/build/tools/getSpace.js +13 -2
- package/build/tools/getSpaceActivities.d.ts +3 -3
- package/build/tools/getSpaceActivities.js +12 -2
- package/build/tools/getUserRecentUpdates.d.ts +3 -3
- package/build/tools/getUserRecentUpdates.js +12 -2
- package/build/tools/getUserStarsCount.d.ts +3 -3
- package/build/tools/getUserStarsCount.js +3 -2
- package/build/tools/getUsers.d.ts +3 -3
- package/build/tools/getUsers.js +11 -2
- package/build/tools/getVersionMilestoneList.d.ts +3 -3
- package/build/tools/getVersionMilestoneList.js +12 -2
- package/build/tools/getWatchingListCount.d.ts +3 -3
- package/build/tools/getWatchingListCount.js +5 -2
- package/build/tools/getWatchingListItems.d.ts +3 -3
- package/build/tools/getWatchingListItems.js +12 -2
- package/build/tools/getWiki.d.ts +3 -3
- package/build/tools/getWiki.js +16 -2
- package/build/tools/getWikiPages.d.ts +3 -3
- package/build/tools/getWikiPages.js +12 -2
- package/build/tools/getWikisCount.d.ts +3 -3
- package/build/tools/getWikisCount.js +3 -2
- package/build/tools/markNotificationAsRead.d.ts +7 -2
- package/build/tools/markNotificationAsRead.js +6 -1
- package/build/tools/markWatchingAsRead.d.ts +7 -2
- package/build/tools/markWatchingAsRead.js +6 -1
- package/build/tools/removeRelatedIssue.d.ts +3 -3
- package/build/tools/removeRelatedIssue.js +33 -2
- package/build/tools/resetUnreadNotificationCount.d.ts +3 -3
- package/build/tools/resetUnreadNotificationCount.js +5 -2
- package/build/tools/shared/customFieldFiltersSchema.d.ts +1 -1
- package/build/tools/updateIssue.d.ts +3 -3
- package/build/tools/updateIssue.js +32 -2
- package/build/tools/updateIssueComment.d.ts +3 -3
- package/build/tools/updateIssueComment.js +14 -2
- package/build/tools/updateProject.d.ts +3 -3
- package/build/tools/updateProject.js +22 -2
- package/build/tools/updatePullRequest.d.ts +3 -3
- package/build/tools/updatePullRequest.js +26 -2
- package/build/tools/updatePullRequestComment.d.ts +3 -3
- package/build/tools/updatePullRequestComment.js +12 -2
- package/build/tools/updateVersionMilestone.d.ts +3 -3
- package/build/tools/updateVersionMilestone.js +12 -2
- package/build/tools/updateWatching.d.ts +3 -3
- package/build/tools/updateWatching.js +12 -2
- package/build/tools/updateWiki.d.ts +3 -3
- package/build/tools/updateWiki.js +16 -2
- package/build/types/outputFields.d.ts +26 -0
- package/build/types/outputFields.js +3 -0
- package/build/types/tool.d.ts +22 -9
- package/build/types/zod/activityType.d.ts +38 -0
- package/build/types/zod/activityType.js +38 -0
- package/build/utils/fieldSelection.d.ts +22 -0
- package/build/utils/fieldSelection.js +40 -0
- package/build/utils/logger.js +41 -3
- package/build/utils/resolveIdOrKey.d.ts +2 -2
- package/build/utils/toolsetUtils.d.ts +1 -3
- package/build/utils/toolsetUtils.js +0 -12
- package/package.json +17 -25
- package/build/tools/dynamicTools/toolsets.d.ts +0 -15
- package/build/tools/dynamicTools/toolsets.js +0 -103
- package/build/types/zod/backlogOutputDefinition.d.ts +0 -3334
- package/build/types/zod/backlogOutputDefinition.js +0 -483
- package/build/utils/generateFieldsDescription.d.ts +0 -5
- package/build/utils/generateFieldsDescription.js +0 -50
- package/build/utils/toolRegistrar.d.ts +0 -5
- 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
|
-
-
|
|
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
|
-
####
|
|
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: "
|
|
443
|
+
get_project(projectIdOrKey: "PROJECT-KEY", fields: ["name", "key", "description"])
|
|
456
444
|
```
|
|
457
445
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
```
|
|
461
|
-
get_project(projectIdOrKey: "PROJECT-KEY", fields: "{ name key description }")
|
|
462
|
-
```
|
|
446
|
+
`fields` を省略すると結果全体が返ります。選択は 1 階層のみで、オブジェクトや配列のフィールドを指定した場合はそのまま丸ごと返ります。
|
|
463
447
|
|
|
464
448
|
利点:
|
|
465
449
|
|
|
@@ -485,6 +469,22 @@ MAX_TOKENS=10000
|
|
|
485
469
|
|
|
486
470
|
> 注:これはベストエフォートの緩和策であり、保証された強制ではありません。
|
|
487
471
|
|
|
472
|
+
### ログ出力
|
|
473
|
+
|
|
474
|
+
ログは **stderr** に出力されます(stdio トランスポートでは stdout が JSON-RPC のストリームに使われるため)。
|
|
475
|
+
|
|
476
|
+
| 変数 | 説明 |
|
|
477
|
+
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
478
|
+
| `LOG_LEVEL` | `fatal` / `error` / `warn` / `info` / `debug` / `trace` / `silent`。既定は `NODE_ENV` が `production`(未設定時もこれに含まれる)のとき `error`、それ以外は `debug`。認識できない値は通知した上で既定値にフォールバックします。 |
|
|
479
|
+
|
|
480
|
+
出力**フォーマット**は引き続き `NODE_ENV` が決めます(`production` 以外では `pino-pretty` が利用可能なら人間向けの整形出力に切り替わります)。ログ量を変えたいときは `NODE_ENV` ではなく `LOG_LEVEL` を設定してください。デプロイ先で構造化 JSON のまま保てます。
|
|
481
|
+
|
|
482
|
+
`pino-pretty` は開発用の依存なので、公開されている npm パッケージにもコンテナイメージにも含まれていません。そのため、これらでは `NODE_ENV` の値に関わらず常に構造化 JSON が出力され、出力を変えられるのは `LOG_LEVEL` だけです。
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
LOG_LEVEL=info node build/index.js --transport http
|
|
486
|
+
```
|
|
487
|
+
|
|
488
488
|
### 完全なカスタム設定例
|
|
489
489
|
|
|
490
490
|
このセクションでは、複数の環境変数を使用した高度な設定を示します。これらは実験的な機能であり、すべてのMCPクライアントでサポートされているとは限りません。これはMCP標準仕様の一部ではなく、注意して使用する必要があります。
|
|
@@ -518,8 +518,7 @@ MAX_TOKENS=10000
|
|
|
518
518
|
"MAX_TOKENS": "10000",
|
|
519
519
|
"OPTIMIZE_RESPONSE": "1",
|
|
520
520
|
"PREFIX": "backlog_",
|
|
521
|
-
"ENABLE_TOOLSETS": "space,project,issue"
|
|
522
|
-
"ENABLE_DYNAMIC_TOOLSETS": "1"
|
|
521
|
+
"ENABLE_TOOLSETS": "space,project,issue"
|
|
523
522
|
}
|
|
524
523
|
}
|
|
525
524
|
}
|
|
@@ -546,7 +545,7 @@ pnpm test
|
|
|
546
545
|
サーバーはいくつかのコマンドラインオプションをサポートしています:
|
|
547
546
|
|
|
548
547
|
- `--export-descriptions`: ツール一覧の構築時に解決される説明キーと値をエクスポート。旧名は `--export-translations` で、非推奨エイリアスとして当面動作しますが、将来のリリースで削除されます
|
|
549
|
-
- `--optimize-response`:
|
|
548
|
+
- `--optimize-response`: 各ツールに、返す結果フィールドを選ぶ `fields` パラメータを追加する
|
|
550
549
|
- `--max-tokens=NUMBER`: レスポンスの最大トークン制限を設定
|
|
551
550
|
- `--prefix=STRING`: すべてのツール名に付加するオプションの文字列プレフィックス(デフォルト:"")
|
|
552
551
|
- `--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
|
-
-
|
|
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
|
|
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
|
-
|
|
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: "
|
|
559
|
+
get_project(projectIdOrKey: "PROJECT-KEY", fields: ["name", "key", "description"])
|
|
568
560
|
```
|
|
569
561
|
|
|
570
|
-
|
|
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
|
|
|
@@ -597,6 +586,22 @@ If a response exceeds the limit, it will be truncated with a warning.
|
|
|
597
586
|
|
|
598
587
|
> Note: This is a best-effort mitigation, not a guaranteed enforcement.
|
|
599
588
|
|
|
589
|
+
### Logging
|
|
590
|
+
|
|
591
|
+
The server logs to **stderr** (stdout carries the JSON-RPC stream on the stdio transport).
|
|
592
|
+
|
|
593
|
+
| Variable | Description |
|
|
594
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
595
|
+
| `LOG_LEVEL` | `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`. Defaults to `error` when `NODE_ENV` is `production` — which is also the default when `NODE_ENV` is unset — and to `debug` otherwise. An unrecognised value is reported and the default is used. |
|
|
596
|
+
|
|
597
|
+
`NODE_ENV` still selects the output *format*: any value other than `production` switches to human-readable `pino-pretty` output when that package is available. Use `LOG_LEVEL`, not `NODE_ENV`, to change how much is logged, so that a deployment keeps structured JSON:
|
|
598
|
+
|
|
599
|
+
`pino-pretty` is a development dependency, so neither the published npm package nor the container image carries a copy. In those, logs are structured JSON whatever `NODE_ENV` says, and `LOG_LEVEL` is the only setting that changes the output.
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
LOG_LEVEL=info node build/index.js --transport http
|
|
603
|
+
```
|
|
604
|
+
|
|
600
605
|
### Full Custom Configuration Example
|
|
601
606
|
|
|
602
607
|
This section demonstrates advanced configuration using multiple environment variables. These are experimental features and may not be supported across all MCP clients. This is not part of the MCP standard specification and should be used with caution.
|
|
@@ -630,8 +635,7 @@ This section demonstrates advanced configuration using multiple environment vari
|
|
|
630
635
|
"MAX_TOKENS": "10000",
|
|
631
636
|
"OPTIMIZE_RESPONSE": "1",
|
|
632
637
|
"PREFIX": "backlog_",
|
|
633
|
-
"ENABLE_TOOLSETS": "space,project,issue"
|
|
634
|
-
"ENABLE_DYNAMIC_TOOLSETS": "1"
|
|
638
|
+
"ENABLE_TOOLSETS": "space,project,issue"
|
|
635
639
|
}
|
|
636
640
|
}
|
|
637
641
|
}
|
|
@@ -663,7 +667,7 @@ The server supports several command line options:
|
|
|
663
667
|
- `--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
668
|
- `--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
669
|
- `--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`:
|
|
670
|
+
- `--optimize-response`: Add a `fields` parameter to each tool for selecting which result fields to return
|
|
667
671
|
- `--max-tokens=NUMBER`: Set maximum token limit for responses
|
|
668
672
|
- `--prefix=STRING`: Optional string prefix to prepend to all tool names (default: "")
|
|
669
673
|
- `--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 (
|
|
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: ${JSON.stringify(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';
|
|
@@ -49,20 +49,23 @@ export function customFieldFiltersToPayload(customFields) {
|
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
case 'list': {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
// Always an array, single value included. Backlog matches a list custom
|
|
53
|
+
// field only on the indexed parameters `customField_1[0]=…`, which
|
|
54
|
+
// `Request.toQueryString` produces from an array under a `customField_`
|
|
55
|
+
// key. A bare `customField_1=…` is not matched, and neither is the
|
|
56
|
+
// `customField_1[][0]=…` that a `[]` suffix on this key would produce.
|
|
57
|
+
// Neither is rejected either — the filter is silently ignored and every
|
|
58
|
+
// issue comes back, so the caller cannot tell a filtered query from an
|
|
59
|
+
// unfiltered one.
|
|
60
|
+
const values = (Array.isArray(field.value) ? field.value : [field.value]).filter((value) => Number.isFinite(value));
|
|
61
|
+
if (values.length > 0) {
|
|
62
|
+
result[baseKey] = values;
|
|
60
63
|
}
|
|
61
64
|
break;
|
|
62
65
|
}
|
|
63
66
|
default: {
|
|
64
67
|
const exhaustiveCheck = field;
|
|
65
|
-
throw new Error(`Unsupported custom field filter type: ${exhaustiveCheck}`);
|
|
68
|
+
throw new Error(`Unsupported custom field filter type: ${JSON.stringify(exhaustiveCheck)}`);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
}
|
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
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,
|
|
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
|
|
11
|
-
// and environment
|
|
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,
|
|
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
|
-
},
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
-
|
|
21
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
if (Array.isArray(resultData)) {
|
|
22
|
+
const data = result.data;
|
|
23
|
+
if (Array.isArray(data)) {
|
|
13
24
|
return {
|
|
14
25
|
kind: 'ok',
|
|
15
|
-
data:
|
|
26
|
+
data: data.map((item) => pick(item, fields)),
|
|
16
27
|
};
|
|
17
28
|
}
|
|
18
|
-
|
|
29
|
+
if (typeof data === 'object' && data !== null) {
|
|
19
30
|
return {
|
|
20
31
|
kind: 'ok',
|
|
21
|
-
data:
|
|
32
|
+
data: pick(data, fields),
|
|
22
33
|
};
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
35
|
+
// A scalar has no fields to narrow.
|
|
36
|
+
return result;
|
|
27
37
|
};
|
|
28
38
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
48
|
+
return picked;
|
|
55
49
|
}
|
package/build/httpMcpServer.js
CHANGED
|
@@ -16,6 +16,14 @@ export const runHttpMcpServer = async (options) => {
|
|
|
16
16
|
const app = new Hono();
|
|
17
17
|
const isLocalhostBind = LOCALHOST_BINDS.includes(host);
|
|
18
18
|
const oauthEnabled = !!(oauthConfig && tokenStore);
|
|
19
|
+
// Registered before the Host and Origin guards so that it stays reachable
|
|
20
|
+
// under an allow list. A load balancer health check addresses the target
|
|
21
|
+
// directly and puts its address in `Host` — AWS ALB gives no way to override
|
|
22
|
+
// it — so a guarded `/health` can only ever answer 403, leaving an operator
|
|
23
|
+
// to choose between dropping the allow list and treating 403 as healthy.
|
|
24
|
+
// Exempting it costs nothing: the response is constant and reaches no state,
|
|
25
|
+
// so there is nothing here for a rebinding page to reach either.
|
|
26
|
+
app.get('/health', (c) => c.json({ status: 'healthy', timestamp: new Date().toISOString(), version }));
|
|
19
27
|
// DNS rebinding protection. `Host` is the actual defense: a rebinding page
|
|
20
28
|
// reaches us carrying its own hostname, which the allow list rejects.
|
|
21
29
|
if (allowedHosts?.length) {
|
|
@@ -36,7 +44,6 @@ export const runHttpMcpServer = async (options) => {
|
|
|
36
44
|
else if (isLocalhostBind && !allowedHosts?.length) {
|
|
37
45
|
app.use('*', localhostOriginValidation());
|
|
38
46
|
}
|
|
39
|
-
app.get('/health', (c) => c.json({ status: 'healthy', timestamp: new Date().toISOString(), version }));
|
|
40
47
|
if (oauthEnabled) {
|
|
41
48
|
const { createOAuthRoutes } = await import('./auth/oauthRoutes.js');
|
|
42
49
|
const { createBearerAuthMiddleware } = await import('./auth/bearerAuthMiddleware.js');
|