backlog-mcp-server 0.9.0 → 0.10.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.
- package/README.ja.md +87 -0
- package/README.md +83 -0
- package/build/handlers/builders/composeToolHandler.js +16 -8
- package/build/handlers/transformers/wrapWithOrganizationContext.js +7 -0
- package/build/index.js +5 -4
- package/build/tools/dynamicTools/organizations.js +38 -0
- package/build/tools/dynamicTools/toolsets.js +1 -1
- package/build/tools/tools.js +0 -2
- package/build/utils/backlogClientRegistry.js +132 -0
- package/build/utils/backlogOrganizationContext.js +8 -0
- package/package.json +2 -2
- package/build/tools/deleteDocument.js +0 -19
package/README.ja.md
CHANGED
|
@@ -507,6 +507,93 @@ npm test
|
|
|
507
507
|
node build/index.js --optimize-response --max-tokens=100000 --prefix="backlog_" --enable-toolsets space,issue
|
|
508
508
|
```
|
|
509
509
|
|
|
510
|
+
## 複数組織対応
|
|
511
|
+
|
|
512
|
+
このサーバーは、1つのMCPサーバーインスタンスから複数のBacklog組織にアクセスできるよう設定できます。
|
|
513
|
+
|
|
514
|
+
### 設定
|
|
515
|
+
|
|
516
|
+
組織ごとに環境変数のペアを定義し、デフォルト組織を設定します。
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
BACKLOG_DEFAULT_ORG=COMPANY_A
|
|
520
|
+
BACKLOG_ORG_COMPANY_A_DOMAIN=company-a.backlog.com
|
|
521
|
+
BACKLOG_ORG_COMPANY_A_API_KEY=your-company-a-api-key
|
|
522
|
+
BACKLOG_ORG_COMPANY_B_DOMAIN=company-b.backlog.com
|
|
523
|
+
BACKLOG_ORG_COMPANY_B_API_KEY=your-company-b-api-key
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
これらの変数は、ローカルの`.env`、シェル環境変数、またはMCPクライアント設定の`env`ブロックのいずれからでも利用できます。
|
|
527
|
+
|
|
528
|
+
MCP設定例:
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"mcpServers": {
|
|
533
|
+
"backlog": {
|
|
534
|
+
"env": {
|
|
535
|
+
"BACKLOG_DEFAULT_ORG": "COMPANY_A",
|
|
536
|
+
"BACKLOG_ORG_COMPANY_A_DOMAIN": "company-a.backlog.com",
|
|
537
|
+
"BACKLOG_ORG_COMPANY_A_API_KEY": "your-company-a-api-key",
|
|
538
|
+
"BACKLOG_ORG_COMPANY_B_DOMAIN": "company-b.backlog.com",
|
|
539
|
+
"BACKLOG_ORG_COMPANY_B_API_KEY": "your-company-b-api-key"
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
複数組織用の環境変数が設定されていない場合、サーバーは従来どおり単一組織用の設定にフォールバックします。
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
BACKLOG_DOMAIN=your-domain.backlog.com
|
|
550
|
+
BACKLOG_API_KEY=your-api-key
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### ツールの使い方
|
|
554
|
+
|
|
555
|
+
通常のツールはすべて、任意の`organization`入力フィールドを受け付けます。指定した場合、そのBacklog組織に対してツールが実行されます。
|
|
556
|
+
|
|
557
|
+
例:
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
{
|
|
561
|
+
"organization": "COMPANY_B",
|
|
562
|
+
"projectKey": "PROJECT"
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
`organization`を省略した場合:
|
|
567
|
+
|
|
568
|
+
- `BACKLOG_DEFAULT_ORG`で指定した組織が使われます
|
|
569
|
+
- 複数組織用の環境変数が存在するのに`BACKLOG_DEFAULT_ORG`が未設定の場合、サーバーは起動時に失敗します
|
|
570
|
+
|
|
571
|
+
### 組織一覧の確認
|
|
572
|
+
|
|
573
|
+
サーバーは `list_organizations` ツールを提供しており、設定済みの組織名、ドメイン、デフォルト組織かどうかを返します。
|
|
574
|
+
|
|
575
|
+
レスポンス例:
|
|
576
|
+
|
|
577
|
+
```json
|
|
578
|
+
[
|
|
579
|
+
{
|
|
580
|
+
"name": "COMPANY_A",
|
|
581
|
+
"domain": "company-a.backlog.com",
|
|
582
|
+
"isDefault": true
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
"name": "COMPANY_B",
|
|
586
|
+
"domain": "company-b.backlog.com",
|
|
587
|
+
"isDefault": false
|
|
588
|
+
}
|
|
589
|
+
]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### 注意
|
|
593
|
+
|
|
594
|
+
- 複数組織モードでは、各組織に対して `BACKLOG_ORG_<NAME>_DOMAIN` と `BACKLOG_ORG_<NAME>_API_KEY` の両方を定義する必要があります
|
|
595
|
+
- `<NAME>` の部分が、`organization`入力や `list_organizations` に表示される組織名になります
|
|
596
|
+
|
|
510
597
|
## ライセンス
|
|
511
598
|
|
|
512
599
|
このプロジェクトは [MITライセンス](./LICENSE) のもとでライセンスされています。
|
package/README.md
CHANGED
|
@@ -587,6 +587,89 @@ Example:
|
|
|
587
587
|
node build/index.js --optimize-response --max-tokens=100000 --prefix="backlog_" --enable-toolsets space,issue
|
|
588
588
|
```
|
|
589
589
|
|
|
590
|
+
## Multi-Organization Support
|
|
591
|
+
|
|
592
|
+
This server can be configured to access multiple Backlog organizations from a single MCP server instance.
|
|
593
|
+
|
|
594
|
+
### Configuration
|
|
595
|
+
|
|
596
|
+
Configure one env pair per organization and set a default organization:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
BACKLOG_DEFAULT_ORG=COMPANY_A
|
|
600
|
+
BACKLOG_ORG_COMPANY_A_DOMAIN=company-a.backlog.com
|
|
601
|
+
BACKLOG_ORG_COMPANY_A_API_KEY=your-company-a-api-key
|
|
602
|
+
BACKLOG_ORG_COMPANY_B_DOMAIN=company-b.backlog.com
|
|
603
|
+
BACKLOG_ORG_COMPANY_B_API_KEY=your-company-b-api-key
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
This works whether the variables come from a local `.env`, your shell environment, or an MCP client config `env` block.
|
|
607
|
+
|
|
608
|
+
Example MCP config:
|
|
609
|
+
|
|
610
|
+
```json
|
|
611
|
+
{
|
|
612
|
+
"env": {
|
|
613
|
+
"BACKLOG_DEFAULT_ORG": "COMPANY_A",
|
|
614
|
+
"BACKLOG_ORG_COMPANY_A_DOMAIN": "company-a.backlog.com",
|
|
615
|
+
"BACKLOG_ORG_COMPANY_A_API_KEY": "your-company-a-api-key",
|
|
616
|
+
"BACKLOG_ORG_COMPANY_B_DOMAIN": "company-b.backlog.com",
|
|
617
|
+
"BACKLOG_ORG_COMPANY_B_API_KEY": "your-company-b-api-key"
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
If no multi-organization env vars are set, the server falls back to the existing single-organization configuration:
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
BACKLOG_DOMAIN=your-domain.backlog.com
|
|
626
|
+
BACKLOG_API_KEY=your-api-key
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Tool Usage
|
|
630
|
+
|
|
631
|
+
All normal tools accept an optional `organization` input field. When provided, the tool call is routed to that Backlog organization.
|
|
632
|
+
|
|
633
|
+
Examples:
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"organization": "COMPANY_B",
|
|
638
|
+
"projectKey": "PROJECT"
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
If `organization` is omitted:
|
|
643
|
+
|
|
644
|
+
- the organization named by `BACKLOG_DEFAULT_ORG` is used
|
|
645
|
+
- if multi-organization env vars are present and `BACKLOG_DEFAULT_ORG` is missing, the server fails at startup
|
|
646
|
+
|
|
647
|
+
### Organization Discovery
|
|
648
|
+
|
|
649
|
+
The server provides a `list_organizations` tool that returns the configured organization names, their domains, and which one is the default.
|
|
650
|
+
|
|
651
|
+
Example response:
|
|
652
|
+
|
|
653
|
+
```json
|
|
654
|
+
[
|
|
655
|
+
{
|
|
656
|
+
"name": "COMPANY_A",
|
|
657
|
+
"domain": "company-a.backlog.com",
|
|
658
|
+
"isDefault": true
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"name": "COMPANY_B",
|
|
662
|
+
"domain": "company-b.backlog.com",
|
|
663
|
+
"isDefault": false
|
|
664
|
+
}
|
|
665
|
+
]
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Notes
|
|
669
|
+
|
|
670
|
+
- For multi-org mode, every organization must define both `BACKLOG_ORG_<NAME>_DOMAIN` and `BACKLOG_ORG_<NAME>_API_KEY`.
|
|
671
|
+
- The `<NAME>` part is the organization name exposed through the `organization` tool input and `list_organizations`.
|
|
672
|
+
|
|
590
673
|
## License
|
|
591
674
|
|
|
592
675
|
This project is licensed under the [MIT License](./LICENSE).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { wrapWithErrorHandling } from '../transformers/wrapWithErrorHandling.js';
|
|
3
3
|
import { wrapWithFieldPicking } from '../transformers/wrapWithFieldPicking.js';
|
|
4
|
+
import { wrapWithOrganizationContext } from '../transformers/wrapWithOrganizationContext.js';
|
|
4
5
|
import { wrapWithTokenLimit } from '../transformers/wrapWithTokenLimit.js';
|
|
5
6
|
import { wrapWithToolResult } from '../transformers/wrapWithToolResult.js';
|
|
6
7
|
import { z } from 'zod';
|
|
@@ -8,19 +9,26 @@ import { generateFieldsDescription } from '../../utils/generateFieldsDescription
|
|
|
8
9
|
export function composeToolHandler(tool, options) {
|
|
9
10
|
const { useFields, errorHandler, maxTokens } = options;
|
|
10
11
|
// Step 1: Add `fields` to schema if needed
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const fieldDesc = useFields
|
|
13
|
+
? generateFieldsDescription(tool.outputSchema, tool.importantFields ?? [], tool.name)
|
|
14
|
+
: undefined;
|
|
15
|
+
tool.schema = extendSchema(tool.schema, fieldDesc);
|
|
15
16
|
// Step 2: Compose
|
|
16
|
-
let handler = wrapWithErrorHandling(tool.handler, errorHandler);
|
|
17
|
+
let handler = wrapWithErrorHandling(wrapWithOrganizationContext(tool.handler), errorHandler);
|
|
17
18
|
if (useFields) {
|
|
18
19
|
handler = wrapWithFieldPicking(handler);
|
|
19
20
|
}
|
|
20
21
|
return wrapWithToolResult(wrapWithTokenLimit(handler, maxTokens));
|
|
21
22
|
}
|
|
22
23
|
function extendSchema(schema, desc) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const extension = {
|
|
25
|
+
organization: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Optional organization name. Use list_organizations to inspect available organizations.'),
|
|
29
|
+
};
|
|
30
|
+
if (desc) {
|
|
31
|
+
extension.fields = z.string().describe(desc);
|
|
32
|
+
}
|
|
33
|
+
return schema.extend(extension);
|
|
26
34
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { runWithOrganization } from '../../utils/backlogOrganizationContext.js';
|
|
2
|
+
export function wrapWithOrganizationContext(fn) {
|
|
3
|
+
return async (input) => {
|
|
4
|
+
const { organization, ...rest } = input;
|
|
5
|
+
return runWithOrganization(organization, () => fn(rest));
|
|
6
|
+
};
|
|
7
|
+
}
|
package/build/index.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
// Licensed under the MIT License.
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
-
import * as backlogjs from 'backlog-js';
|
|
7
6
|
import dotenv from 'dotenv';
|
|
8
7
|
import { default as env } from 'env-var';
|
|
9
8
|
import yargs from 'yargs';
|
|
10
9
|
import { hideBin } from 'yargs/helpers';
|
|
11
10
|
import { createTranslationHelper } from './createTranslationHelper.js';
|
|
12
11
|
import { registerDynamicTools, registerTools } from './registerTools.js';
|
|
12
|
+
import { organizationTools } from './tools/dynamicTools/organizations.js';
|
|
13
13
|
import { dynamicTools } from './tools/dynamicTools/toolsets.js';
|
|
14
|
+
import { createBacklogClientRegistry } from './utils/backlogClientRegistry.js';
|
|
14
15
|
import { logger } from './utils/logger.js';
|
|
15
16
|
import { createToolRegistrar } from './utils/toolRegistrar.js';
|
|
16
17
|
import { buildToolsetGroup } from './utils/toolsetUtils.js';
|
|
@@ -18,9 +19,6 @@ import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.j
|
|
|
18
19
|
import packageJson from '../package.json' with { type: 'json' };
|
|
19
20
|
const { version } = packageJson;
|
|
20
21
|
dotenv.config();
|
|
21
|
-
const domain = env.get('BACKLOG_DOMAIN').required().asString();
|
|
22
|
-
const apiKey = env.get('BACKLOG_API_KEY').required().asString();
|
|
23
|
-
const backlog = new backlogjs.Backlog({ host: domain, apiKey: apiKey });
|
|
24
22
|
const argv = yargs(hideBin(process.argv))
|
|
25
23
|
.option('max-tokens', {
|
|
26
24
|
type: 'number',
|
|
@@ -60,6 +58,8 @@ Available toolsets:
|
|
|
60
58
|
default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(),
|
|
61
59
|
})
|
|
62
60
|
.parseSync();
|
|
61
|
+
const clientRegistry = createBacklogClientRegistry();
|
|
62
|
+
const backlog = clientRegistry.createScopedClient();
|
|
63
63
|
const useFields = argv.optimizeResponse;
|
|
64
64
|
const server = wrapServerWithToolRegistry(new McpServer({
|
|
65
65
|
name: 'backlog',
|
|
@@ -78,6 +78,7 @@ const mcpOption = { useFields: useFields, maxTokens, prefix };
|
|
|
78
78
|
const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets);
|
|
79
79
|
// Register all tools
|
|
80
80
|
registerTools(server, toolsetGroup, mcpOption);
|
|
81
|
+
registerDynamicTools(server, organizationTools(clientRegistry, transHelper), prefix);
|
|
81
82
|
// Register dynamic tool management tools if enabled
|
|
82
83
|
if (argv.dynamicToolsets) {
|
|
83
84
|
const registrar = createToolRegistrar(server, toolsetGroup, mcpOption);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function organizationTools(registry, { t }) {
|
|
3
|
+
return {
|
|
4
|
+
toolsets: [
|
|
5
|
+
{
|
|
6
|
+
name: 'organization_metadata',
|
|
7
|
+
description: 'Tools for inspecting configured Backlog organizations.',
|
|
8
|
+
enabled: true,
|
|
9
|
+
tools: [listOrganizationsTool(registry, t)],
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function listOrganizationsTool(registry, t) {
|
|
15
|
+
return {
|
|
16
|
+
name: 'list_organizations',
|
|
17
|
+
description: t('TOOL_LIST_ORGANIZATIONS_DESCRIPTION', 'List configured Backlog organizations and identify the default organization.'),
|
|
18
|
+
schema: z.object({}),
|
|
19
|
+
handler: async () => {
|
|
20
|
+
const organizations = registry.listOrganizations().map(toToolOutput);
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify(organizations, null, 2),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function toToolOutput(organization) {
|
|
33
|
+
return {
|
|
34
|
+
name: organization.name,
|
|
35
|
+
domain: organization.domain,
|
|
36
|
+
isDefault: organization.isDefault,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -24,7 +24,7 @@ const enableToolsetSchema = buildToolSchema((t) => ({
|
|
|
24
24
|
export const enableToolsetTool = (toolRegistrar, { t }) => {
|
|
25
25
|
return {
|
|
26
26
|
name: 'enable_toolset',
|
|
27
|
-
description: t('TOOL_ENABLE_TOOLSET_DESCRIPTION', 'Enable one of the
|
|
27
|
+
description: t('TOOL_ENABLE_TOOLSET_DESCRIPTION', 'Enable one of the Backlog MCP server toolsets. Use get_toolset_tools and list_available_toolsets first to inspect what this will enable.'),
|
|
28
28
|
schema: z.object(enableToolsetSchema(t)),
|
|
29
29
|
handler: async ({ toolset }) => {
|
|
30
30
|
const msg = await toolRegistrar.enableToolsetAndRefresh(toolset);
|
package/build/tools/tools.js
CHANGED
|
@@ -55,7 +55,6 @@ import { addVersionMilestoneTool } from './addVersionMilestone.js';
|
|
|
55
55
|
import { updateVersionMilestoneTool } from './updateVersionMilestone.js';
|
|
56
56
|
import { deleteVersionTool } from './deleteVersion.js';
|
|
57
57
|
import { addDocumentTool } from './addDocument.js';
|
|
58
|
-
import { deleteDocumentTool } from './deleteDocument.js';
|
|
59
58
|
export const allTools = (backlog, helper) => {
|
|
60
59
|
return {
|
|
61
60
|
toolsets: [
|
|
@@ -152,7 +151,6 @@ export const allTools = (backlog, helper) => {
|
|
|
152
151
|
getDocumentTreeTool(backlog, helper),
|
|
153
152
|
getDocumentTool(backlog, helper),
|
|
154
153
|
addDocumentTool(backlog, helper),
|
|
155
|
-
deleteDocumentTool(backlog, helper),
|
|
156
154
|
],
|
|
157
155
|
},
|
|
158
156
|
{
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Backlog } from 'backlog-js';
|
|
2
|
+
import { getCurrentOrganization } from './backlogOrganizationContext.js';
|
|
3
|
+
export function createBacklogClientRegistry(input = {}) {
|
|
4
|
+
const env = input.env ?? process.env;
|
|
5
|
+
const multiOrgRegistry = createMultiOrganizationRegistryFromEnv(env);
|
|
6
|
+
if (multiOrgRegistry) {
|
|
7
|
+
return multiOrgRegistry;
|
|
8
|
+
}
|
|
9
|
+
const domain = env.BACKLOG_DOMAIN;
|
|
10
|
+
const apiKey = env.BACKLOG_API_KEY;
|
|
11
|
+
if (!domain || !apiKey) {
|
|
12
|
+
throw new Error('Configure either BACKLOG_ORG_<NAME>_DOMAIN and BACKLOG_ORG_<NAME>_API_KEY with BACKLOG_DEFAULT_ORG, or both BACKLOG_DOMAIN and BACKLOG_API_KEY.');
|
|
13
|
+
}
|
|
14
|
+
const defaultName = 'default';
|
|
15
|
+
const client = new Backlog({ host: domain, apiKey });
|
|
16
|
+
const info = {
|
|
17
|
+
name: defaultName,
|
|
18
|
+
domain,
|
|
19
|
+
isDefault: true,
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
resolveClient: (organization) => {
|
|
23
|
+
if (organization && organization !== defaultName) {
|
|
24
|
+
throw new Error(`Unknown organization '${organization}'. Use list_organizations to inspect available organizations.`);
|
|
25
|
+
}
|
|
26
|
+
return client;
|
|
27
|
+
},
|
|
28
|
+
createScopedClient: () => createBacklogClientProxy(() => {
|
|
29
|
+
const organization = getCurrentOrganization();
|
|
30
|
+
if (organization && organization !== defaultName) {
|
|
31
|
+
throw new Error(`Unknown organization '${organization}'. Use list_organizations to inspect available organizations.`);
|
|
32
|
+
}
|
|
33
|
+
return client;
|
|
34
|
+
}),
|
|
35
|
+
listOrganizations: () => [info],
|
|
36
|
+
getDefaultOrganization: () => defaultName,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function createMultiOrganizationRegistryFromEnv(env) {
|
|
40
|
+
const organizations = new Map();
|
|
41
|
+
let hasMultiOrgKeys = false;
|
|
42
|
+
for (const [key, value] of Object.entries(env)) {
|
|
43
|
+
const match = /^BACKLOG_ORG_(.+)_(DOMAIN|API_KEY)$/.exec(key);
|
|
44
|
+
if (!match) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
hasMultiOrgKeys = true;
|
|
48
|
+
const [, organization, field] = match;
|
|
49
|
+
const config = organizations.get(organization) ?? {};
|
|
50
|
+
if (field === 'DOMAIN') {
|
|
51
|
+
config.domain = value;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
config.apiKey = value;
|
|
55
|
+
}
|
|
56
|
+
organizations.set(organization, config);
|
|
57
|
+
}
|
|
58
|
+
if (!hasMultiOrgKeys) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const invalidOrganizations = Array.from(organizations.entries())
|
|
62
|
+
.filter(([, config]) => !config.domain || !config.apiKey)
|
|
63
|
+
.map(([organization, config]) => {
|
|
64
|
+
const missing = [];
|
|
65
|
+
if (!config.domain)
|
|
66
|
+
missing.push(`BACKLOG_ORG_${organization}_DOMAIN`);
|
|
67
|
+
if (!config.apiKey)
|
|
68
|
+
missing.push(`BACKLOG_ORG_${organization}_API_KEY`);
|
|
69
|
+
return `${organization} (missing: ${missing.join(', ')})`;
|
|
70
|
+
})
|
|
71
|
+
.sort();
|
|
72
|
+
if (invalidOrganizations.length > 0) {
|
|
73
|
+
throw new Error(`Incomplete multi-organization configuration. ${invalidOrganizations.join('; ')}`);
|
|
74
|
+
}
|
|
75
|
+
if (organizations.size === 0) {
|
|
76
|
+
throw new Error('No valid multi-organization configuration was found. Define BACKLOG_ORG_<NAME>_DOMAIN and BACKLOG_ORG_<NAME>_API_KEY pairs.');
|
|
77
|
+
}
|
|
78
|
+
const defaultOrganization = env.BACKLOG_DEFAULT_ORG;
|
|
79
|
+
if (!defaultOrganization) {
|
|
80
|
+
throw new Error('BACKLOG_DEFAULT_ORG is required when using BACKLOG_ORG_<NAME>_DOMAIN and BACKLOG_ORG_<NAME>_API_KEY.');
|
|
81
|
+
}
|
|
82
|
+
const clients = new Map();
|
|
83
|
+
// At this point, all organizations have been validated to have both domain and apiKey
|
|
84
|
+
const validatedOrganizations = organizations;
|
|
85
|
+
const organizationInfo = Array.from(validatedOrganizations.entries()).map(([name, config]) => {
|
|
86
|
+
clients.set(name, new Backlog({
|
|
87
|
+
host: config.domain,
|
|
88
|
+
apiKey: config.apiKey,
|
|
89
|
+
}));
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
domain: config.domain,
|
|
93
|
+
isDefault: name === defaultOrganization,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
if (!clients.has(defaultOrganization)) {
|
|
97
|
+
throw new Error(`BACKLOG_DEFAULT_ORG '${defaultOrganization}' does not match any configured organization. Use list_organizations to inspect available organizations.`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
resolveClient: (organization) => {
|
|
101
|
+
const orgName = organization ?? defaultOrganization;
|
|
102
|
+
return resolveKnownClient(clients, orgName);
|
|
103
|
+
},
|
|
104
|
+
createScopedClient: () => createBacklogClientProxy(() => {
|
|
105
|
+
const organization = getCurrentOrganization();
|
|
106
|
+
return organization === undefined
|
|
107
|
+
? resolveKnownClient(clients, defaultOrganization)
|
|
108
|
+
: resolveKnownClient(clients, organization);
|
|
109
|
+
}),
|
|
110
|
+
listOrganizations: () => organizationInfo,
|
|
111
|
+
getDefaultOrganization: () => defaultOrganization,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function resolveKnownClient(clients, organization) {
|
|
115
|
+
const client = clients.get(organization);
|
|
116
|
+
if (!client) {
|
|
117
|
+
throw new Error(`Unknown organization '${organization}'. Use list_organizations to inspect available organizations.`);
|
|
118
|
+
}
|
|
119
|
+
return client;
|
|
120
|
+
}
|
|
121
|
+
function createBacklogClientProxy(resolveClient) {
|
|
122
|
+
return new Proxy({}, {
|
|
123
|
+
get(_target, prop) {
|
|
124
|
+
const client = resolveClient();
|
|
125
|
+
const value = Reflect.get(client, prop);
|
|
126
|
+
if (typeof value === 'function') {
|
|
127
|
+
return value.bind(client);
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const organizationStorage = new AsyncLocalStorage();
|
|
3
|
+
export function runWithOrganization(organization, fn) {
|
|
4
|
+
return organizationStorage.run(organization, fn);
|
|
5
|
+
}
|
|
6
|
+
export function getCurrentOrganization() {
|
|
7
|
+
return organizationStorage.getStore();
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backlog-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"backlog-mcp-server": "./build/index.js"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"build"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
31
31
|
"backlog-js": "^0.16.0",
|
|
32
32
|
"cosmiconfig": "^9.0.0",
|
|
33
33
|
"dotenv": "^16.5.0",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { DocumentItemSchema } from '../types/zod/backlogOutputDefinition.js';
|
|
3
|
-
import { buildToolSchema } from '../types/tool.js';
|
|
4
|
-
const deleteDocumentSchema = buildToolSchema((t) => ({
|
|
5
|
-
documentId: z
|
|
6
|
-
.string()
|
|
7
|
-
.describe(t('TOOL_DELETE_DOCUMENT_DOCUMENT_ID', 'Document ID')),
|
|
8
|
-
}));
|
|
9
|
-
export const deleteDocumentTool = (backlog, { t }) => {
|
|
10
|
-
return {
|
|
11
|
-
name: 'delete_document',
|
|
12
|
-
description: t('TOOL_DELETE_DOCUMENT_DESCRIPTION', 'Permanently deletes a document. This is irreversible (hard delete) and the document cannot be recovered. Unlike the Backlog UI trash feature, this operation does not move the document to trash.'),
|
|
13
|
-
schema: z.object(deleteDocumentSchema(t)),
|
|
14
|
-
outputSchema: DocumentItemSchema,
|
|
15
|
-
handler: async ({ documentId }) => {
|
|
16
|
-
return backlog.deleteDocument(documentId);
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
};
|