backlog-mcp-server 0.18.1 → 0.20.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.md +1 -0
- package/build/createBacklogMcpServer.js +7 -3
- package/build/handlers/builders/composeNativeContentToolHandler.d.ts +147 -0
- package/build/handlers/builders/composeNativeContentToolHandler.js +55 -0
- package/build/index.js +0 -16
- package/build/lib.d.ts +4 -2
- package/build/lib.js +1 -0
- package/build/registerTools.d.ts +1 -2
- package/build/registerTools.js +15 -10
- package/build/tools/addDocument.js +1 -0
- package/build/tools/getDocument.js +1 -0
- package/build/tools/getDocuments.js +1 -0
- package/build/tools/getIssueAttachment.d.ts +16 -0
- package/build/tools/getIssueAttachment.js +345 -0
- package/build/tools/getNotifications.js +3 -0
- package/build/tools/organizations.d.ts +12 -0
- package/build/tools/{dynamicTools/organizations.js → organizations.js} +11 -11
- package/build/tools/tools.js +2 -0
- package/build/types/tool.d.ts +19 -1
- package/build/types/toolsets.d.ts +13 -6
- package/package.json +2 -2
- package/build/tools/dynamicTools/organizations.d.ts +0 -6
package/README.md
CHANGED
|
@@ -294,6 +294,7 @@ Tools for managing projects, categories, custom fields, and issue types.
|
|
|
294
294
|
Tools for managing issues, their comments, and related items like priorities, categories, custom fields, issue types, resolutions, and watching lists.
|
|
295
295
|
|
|
296
296
|
- `get_issue`: Returns information about a specific issue.
|
|
297
|
+
- `get_issue_attachment`: Downloads one attachment of an issue. Returns it as image or embedded resource content, or as base64 with `format: "base64"`.
|
|
297
298
|
- `get_issues`: Returns list of issues.
|
|
298
299
|
- `count_issues`: Returns count of issues.
|
|
299
300
|
- `add_issue`: Creates a new issue in the specified project.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Copyright (c) 2025 Nulab inc.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
3
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
4
|
-
import {
|
|
5
|
-
import { organizationTools } from './tools/
|
|
4
|
+
import { registerTools } from './registerTools.js';
|
|
5
|
+
import { organizationTools } from './tools/organizations.js';
|
|
6
6
|
import { buildToolsetGroup } from './utils/toolsetUtils.js';
|
|
7
7
|
import { wrapServerWithToolRegistry, } from './utils/wrapServerWithToolRegistry.js';
|
|
8
8
|
// The tool list is fixed for the process lifetime: it only depends on CLI flags
|
|
@@ -27,7 +27,11 @@ export function createBacklogMcpServer({ version, useFields, backlog, clientRegi
|
|
|
27
27
|
// is configured; the `organization` parameter its description points at is
|
|
28
28
|
// published under the same condition.
|
|
29
29
|
if (mcpOption.useOrganization) {
|
|
30
|
-
|
|
30
|
+
registerTools(server, organizationTools(clientRegistry, descriptionHelper),
|
|
31
|
+
// `useOrganization: false` regardless of the flag that got us here.
|
|
32
|
+
// `list_organizations` is what a caller reads to learn what may go in
|
|
33
|
+
// `organization`; scoping the answer to one organization is circular.
|
|
34
|
+
{ ...mcpOption, useOrganization: false });
|
|
31
35
|
}
|
|
32
36
|
return server;
|
|
33
37
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ErrorLike } from '../../types/result.js';
|
|
3
|
+
import { NativeContentToolDefinition } from '../../types/tool.js';
|
|
4
|
+
export type ComposeNativeContentOptions = {
|
|
5
|
+
errorHandler?: (err: unknown) => ErrorLike;
|
|
6
|
+
useOrganization?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type NativeContentInput = {
|
|
9
|
+
organization?: string;
|
|
10
|
+
} & Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Builds the schema and handler a dynamic tool is registered with.
|
|
13
|
+
*
|
|
14
|
+
* The counterpart of `composeToolHandler` for tools that produce a
|
|
15
|
+
* `CallToolResult` themselves. Those cannot go through the whole pipeline:
|
|
16
|
+
* field picking, the token limit and `wrapWithToolResult` all assume a JSON
|
|
17
|
+
* value they may reshape, and reshaping is exactly what a tool returning binary
|
|
18
|
+
* content must not allow — a base64 payload cut at the token limit is a corrupt
|
|
19
|
+
* file reported as a success.
|
|
20
|
+
*
|
|
21
|
+
* The two steps that are not about reshaping still apply, and are why this
|
|
22
|
+
* exists rather than registering the handler directly:
|
|
23
|
+
*
|
|
24
|
+
* - `wrapWithOrganizationContext`, without which a dynamic tool ignores
|
|
25
|
+
* `organization` and always talks to the default space;
|
|
26
|
+
* - `wrapWithErrorHandling`, without which a thrown Backlog error escapes as a
|
|
27
|
+
* protocol error rather than an `isError` result, and never reaches the
|
|
28
|
+
* handler that reacts to Backlog rejecting a credential.
|
|
29
|
+
*
|
|
30
|
+
* As in `composeToolHandler`, the returned schema is a fresh object: the tool
|
|
31
|
+
* definition is never mutated, because one toolset group is shared across
|
|
32
|
+
* per-request servers.
|
|
33
|
+
*/
|
|
34
|
+
export declare function composeNativeContentToolHandler(tool: NativeContentToolDefinition<any>, { errorHandler, useOrganization }?: ComposeNativeContentOptions): {
|
|
35
|
+
schema: z.ZodObject<{
|
|
36
|
+
[x: string]: any;
|
|
37
|
+
organization: z.ZodOptional<z.ZodString>;
|
|
38
|
+
} | {
|
|
39
|
+
[x: string]: any;
|
|
40
|
+
organization?: undefined;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
handler: (input: NativeContentInput) => Promise<{
|
|
43
|
+
[x: string]: unknown;
|
|
44
|
+
_meta?: {
|
|
45
|
+
[x: string]: unknown;
|
|
46
|
+
"io.modelcontextprotocol/serverInfo"?: {
|
|
47
|
+
version: string;
|
|
48
|
+
websiteUrl?: string | undefined;
|
|
49
|
+
description?: string | undefined;
|
|
50
|
+
icons?: {
|
|
51
|
+
src: string;
|
|
52
|
+
mimeType?: string | undefined;
|
|
53
|
+
sizes?: string[] | undefined;
|
|
54
|
+
theme?: "dark" | "light" | undefined;
|
|
55
|
+
}[] | undefined;
|
|
56
|
+
name: string;
|
|
57
|
+
title?: string | undefined;
|
|
58
|
+
} | undefined;
|
|
59
|
+
} | undefined;
|
|
60
|
+
content: ({
|
|
61
|
+
type: "text";
|
|
62
|
+
text: string;
|
|
63
|
+
annotations?: {
|
|
64
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
65
|
+
priority?: number | undefined;
|
|
66
|
+
lastModified?: string | undefined;
|
|
67
|
+
} | undefined;
|
|
68
|
+
_meta?: {
|
|
69
|
+
[x: string]: unknown;
|
|
70
|
+
} | undefined;
|
|
71
|
+
} | {
|
|
72
|
+
type: "image";
|
|
73
|
+
data: string;
|
|
74
|
+
mimeType: string;
|
|
75
|
+
annotations?: {
|
|
76
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
77
|
+
priority?: number | undefined;
|
|
78
|
+
lastModified?: string | undefined;
|
|
79
|
+
} | undefined;
|
|
80
|
+
_meta?: {
|
|
81
|
+
[x: string]: unknown;
|
|
82
|
+
} | undefined;
|
|
83
|
+
} | {
|
|
84
|
+
type: "audio";
|
|
85
|
+
data: string;
|
|
86
|
+
mimeType: string;
|
|
87
|
+
annotations?: {
|
|
88
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
89
|
+
priority?: number | undefined;
|
|
90
|
+
lastModified?: string | undefined;
|
|
91
|
+
} | undefined;
|
|
92
|
+
_meta?: {
|
|
93
|
+
[x: string]: unknown;
|
|
94
|
+
} | undefined;
|
|
95
|
+
} | {
|
|
96
|
+
uri: string;
|
|
97
|
+
description?: string | undefined;
|
|
98
|
+
mimeType?: string | undefined;
|
|
99
|
+
size?: number | undefined;
|
|
100
|
+
annotations?: {
|
|
101
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
102
|
+
priority?: number | undefined;
|
|
103
|
+
lastModified?: string | undefined;
|
|
104
|
+
} | undefined;
|
|
105
|
+
_meta?: {
|
|
106
|
+
[x: string]: unknown;
|
|
107
|
+
} | undefined;
|
|
108
|
+
icons?: {
|
|
109
|
+
src: string;
|
|
110
|
+
mimeType?: string | undefined;
|
|
111
|
+
sizes?: string[] | undefined;
|
|
112
|
+
theme?: "dark" | "light" | undefined;
|
|
113
|
+
}[] | undefined;
|
|
114
|
+
name: string;
|
|
115
|
+
title?: string | undefined;
|
|
116
|
+
type: "resource_link";
|
|
117
|
+
} | {
|
|
118
|
+
type: "resource";
|
|
119
|
+
resource: {
|
|
120
|
+
uri: string;
|
|
121
|
+
mimeType?: string | undefined;
|
|
122
|
+
_meta?: {
|
|
123
|
+
[x: string]: unknown;
|
|
124
|
+
} | undefined;
|
|
125
|
+
text: string;
|
|
126
|
+
} | {
|
|
127
|
+
uri: string;
|
|
128
|
+
mimeType?: string | undefined;
|
|
129
|
+
_meta?: {
|
|
130
|
+
[x: string]: unknown;
|
|
131
|
+
} | undefined;
|
|
132
|
+
blob: string;
|
|
133
|
+
};
|
|
134
|
+
annotations?: {
|
|
135
|
+
audience?: ("assistant" | "user")[] | undefined;
|
|
136
|
+
priority?: number | undefined;
|
|
137
|
+
lastModified?: string | undefined;
|
|
138
|
+
} | undefined;
|
|
139
|
+
_meta?: {
|
|
140
|
+
[x: string]: unknown;
|
|
141
|
+
} | undefined;
|
|
142
|
+
})[];
|
|
143
|
+
structuredContent?: unknown;
|
|
144
|
+
isError?: boolean | undefined;
|
|
145
|
+
}>;
|
|
146
|
+
};
|
|
147
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { wrapWithErrorHandling } from '../transformers/wrapWithErrorHandling.js';
|
|
3
|
+
import { wrapWithOrganizationContext } from '../transformers/wrapWithOrganizationContext.js';
|
|
4
|
+
import { isErrorLike } from '../../types/result.js';
|
|
5
|
+
/**
|
|
6
|
+
* Builds the schema and handler a dynamic tool is registered with.
|
|
7
|
+
*
|
|
8
|
+
* The counterpart of `composeToolHandler` for tools that produce a
|
|
9
|
+
* `CallToolResult` themselves. Those cannot go through the whole pipeline:
|
|
10
|
+
* field picking, the token limit and `wrapWithToolResult` all assume a JSON
|
|
11
|
+
* value they may reshape, and reshaping is exactly what a tool returning binary
|
|
12
|
+
* content must not allow — a base64 payload cut at the token limit is a corrupt
|
|
13
|
+
* file reported as a success.
|
|
14
|
+
*
|
|
15
|
+
* The two steps that are not about reshaping still apply, and are why this
|
|
16
|
+
* exists rather than registering the handler directly:
|
|
17
|
+
*
|
|
18
|
+
* - `wrapWithOrganizationContext`, without which a dynamic tool ignores
|
|
19
|
+
* `organization` and always talks to the default space;
|
|
20
|
+
* - `wrapWithErrorHandling`, without which a thrown Backlog error escapes as a
|
|
21
|
+
* protocol error rather than an `isError` result, and never reaches the
|
|
22
|
+
* handler that reacts to Backlog rejecting a credential.
|
|
23
|
+
*
|
|
24
|
+
* As in `composeToolHandler`, the returned schema is a fresh object: the tool
|
|
25
|
+
* definition is never mutated, because one toolset group is shared across
|
|
26
|
+
* per-request servers.
|
|
27
|
+
*/
|
|
28
|
+
export function composeNativeContentToolHandler(
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
tool, { errorHandler, useOrganization = false } = {}) {
|
|
31
|
+
// `extend` even with nothing to add, so the returned schema is always a copy.
|
|
32
|
+
// Handing back `tool.schema` itself would make the invariant above hold only
|
|
33
|
+
// when `useOrganization` is set.
|
|
34
|
+
const schema = tool.schema.extend(useOrganization
|
|
35
|
+
? {
|
|
36
|
+
organization: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Optional organization name. Use list_organizations to inspect available organizations.'),
|
|
40
|
+
}
|
|
41
|
+
: {});
|
|
42
|
+
const handler = wrapWithErrorHandling(wrapWithOrganizationContext(tool.handler), errorHandler);
|
|
43
|
+
return {
|
|
44
|
+
schema,
|
|
45
|
+
handler: async (input) => {
|
|
46
|
+
const result = await handler(input);
|
|
47
|
+
return isErrorLike(result)
|
|
48
|
+
? {
|
|
49
|
+
isError: true,
|
|
50
|
+
content: [{ type: 'text', text: result.message }],
|
|
51
|
+
}
|
|
52
|
+
: result.data;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
package/build/index.js
CHANGED
|
@@ -129,22 +129,6 @@ Available toolsets:
|
|
|
129
129
|
if (hideBin(process.argv).some((arg) => arg.split('=')[0] === '--export-translations')) {
|
|
130
130
|
process.stderr.write('--export-translations is deprecated and will be removed in a future release. Use --export-descriptions.\n');
|
|
131
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
|
-
}
|
|
148
132
|
const clientRegistry = oauthConfig
|
|
149
133
|
? createOAuthBacklogClientRegistry(oauthConfig.backlogDomain)
|
|
150
134
|
: createBacklogClientRegistry();
|
package/build/lib.d.ts
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export { allTools } from './tools/tools.js';
|
|
16
16
|
export { composeToolHandler } from './handlers/builders/composeToolHandler.js';
|
|
17
|
+
export { composeNativeContentToolHandler } from './handlers/builders/composeNativeContentToolHandler.js';
|
|
17
18
|
export { createDescriptionHelper } from './createDescriptionHelper.js';
|
|
18
19
|
export { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
|
|
19
20
|
export { buildToolSchema } from './types/tool.js';
|
|
20
21
|
export { isErrorLike } from './types/result.js';
|
|
21
22
|
export type { ComposeOptions } from './handlers/builders/composeToolHandler.js';
|
|
23
|
+
export type { ComposeNativeContentOptions } from './handlers/builders/composeNativeContentToolHandler.js';
|
|
22
24
|
export type { DescriptionHelper } from './createDescriptionHelper.js';
|
|
23
|
-
export type { ToolDefinition,
|
|
24
|
-
export type { Toolset, ToolsetGroup
|
|
25
|
+
export type { ToolDefinition, NativeContentToolDefinition, } from './types/tool.js';
|
|
26
|
+
export type { Toolset, ToolsetGroup } from './types/toolsets.js';
|
|
25
27
|
export type { ErrorLike, SafeResult } from './types/result.js';
|
package/build/lib.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export { allTools } from './tools/tools.js';
|
|
16
16
|
export { composeToolHandler } from './handlers/builders/composeToolHandler.js';
|
|
17
|
+
export { composeNativeContentToolHandler } from './handlers/builders/composeNativeContentToolHandler.js';
|
|
17
18
|
export { createDescriptionHelper } from './createDescriptionHelper.js';
|
|
18
19
|
export { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
|
|
19
20
|
export { buildToolSchema } from './types/tool.js';
|
package/build/registerTools.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { MCPOptions } from './types/mcp.js';
|
|
2
|
-
import {
|
|
2
|
+
import { ToolsetGroup } from './types/toolsets.js';
|
|
3
3
|
import { BacklogMCPServer } from './utils/wrapServerWithToolRegistry.js';
|
|
4
4
|
export declare function registerTools(server: BacklogMCPServer, toolsetGroup: ToolsetGroup, options: MCPOptions): void;
|
|
5
|
-
export declare function registerDynamicTools(server: BacklogMCPServer, dynamicToolsetGroup: DynamicToolsetGroup, prefix: string): void;
|
package/build/registerTools.js
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
import { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
|
|
2
|
+
import { composeNativeContentToolHandler } from './handlers/builders/composeNativeContentToolHandler.js';
|
|
2
3
|
import { composeToolHandler } from './handlers/builders/composeToolHandler.js';
|
|
3
4
|
export function registerTools(server, toolsetGroup, options) {
|
|
4
5
|
const { useFields, maxTokens, prefix, useOrganization } = options;
|
|
5
6
|
registerToolsets({
|
|
6
7
|
server,
|
|
7
|
-
toolsetGroup,
|
|
8
|
+
toolsets: toolsetGroup.toolsets,
|
|
8
9
|
prefix,
|
|
9
|
-
prepareTool: (tool) =>
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
composeToolHandler(tool, {
|
|
10
|
+
prepareTool: (tool) => composeToolHandler(tool, {
|
|
12
11
|
useFields,
|
|
13
12
|
errorHandler: backlogErrorHandler,
|
|
14
13
|
maxTokens,
|
|
15
14
|
useOrganization,
|
|
16
15
|
}),
|
|
17
16
|
});
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Tools that build their own result, registered from the same toolsets so
|
|
18
|
+
// that `--enable-toolsets` and the prefix cover them too.
|
|
20
19
|
registerToolsets({
|
|
21
20
|
server,
|
|
22
|
-
|
|
21
|
+
toolsets: toolsetGroup.toolsets.map((toolset) => ({
|
|
22
|
+
enabled: toolset.enabled,
|
|
23
|
+
tools: toolset.nativeContentTools ?? [],
|
|
24
|
+
})),
|
|
23
25
|
prefix,
|
|
24
|
-
prepareTool: (tool) => (
|
|
26
|
+
prepareTool: (tool) => composeNativeContentToolHandler(tool, {
|
|
27
|
+
errorHandler: backlogErrorHandler,
|
|
28
|
+
useOrganization,
|
|
29
|
+
}),
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
|
-
function registerToolsets({ server,
|
|
28
|
-
for (const toolset of
|
|
32
|
+
function registerToolsets({ server, toolsets, prefix, prepareTool, }) {
|
|
33
|
+
for (const toolset of toolsets) {
|
|
29
34
|
if (!toolset.enabled) {
|
|
30
35
|
continue;
|
|
31
36
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Backlog } from 'backlog-js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { DescriptionHelper } from '../createDescriptionHelper.js';
|
|
4
|
+
import { NativeContentToolDefinition } from '../types/tool.js';
|
|
5
|
+
declare const getIssueAttachmentSchema: (t: DescriptionHelper['t']) => {
|
|
6
|
+
issueId: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
issueKey: z.ZodOptional<z.ZodString>;
|
|
8
|
+
attachmentId: z.ZodNumber;
|
|
9
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
10
|
+
auto: "auto";
|
|
11
|
+
base64: "base64";
|
|
12
|
+
}>>;
|
|
13
|
+
maxBytes: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
};
|
|
15
|
+
export declare const getIssueAttachmentTool: (backlog: Backlog, { t }: DescriptionHelper) => NativeContentToolDefinition<ReturnType<typeof getIssueAttachmentSchema>>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildToolSchema } from '../types/tool.js';
|
|
3
|
+
import { resolveIdOrKey } from '../utils/resolveIdOrKey.js';
|
|
4
|
+
const MEBIBYTE = 1024 * 1024;
|
|
5
|
+
/**
|
|
6
|
+
* Both limits are about what survives the transport, not about what Backlog
|
|
7
|
+
* allows. Base64 inflates by 4/3, and the MCP SDK caps a single stdio message
|
|
8
|
+
* at 10 MiB by default, so 7 MiB of raw bytes is the point beyond which a
|
|
9
|
+
* successful download would still fail to reach the client.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_MAX_BYTES = 5 * MEBIBYTE;
|
|
12
|
+
const MAX_ATTACHMENT_BYTES = 7 * MEBIBYTE;
|
|
13
|
+
/**
|
|
14
|
+
* The raster types this tool returns as MCP `image` content.
|
|
15
|
+
*
|
|
16
|
+
* Narrower than the raster types Backlog serves, and the limit
|
|
17
|
+
* does not come from MCP — `ImageContent.mimeType` is an unconstrained string
|
|
18
|
+
* in the schema. It comes from the far end: a Claude-backed host forwards the
|
|
19
|
+
* block to the Messages API, which documents support for `image/jpeg`,
|
|
20
|
+
* `image/png`, `image/gif` and `image/webp` and nothing else
|
|
21
|
+
* (https://platform.claude.com/docs/en/build-with-claude/vision). An
|
|
22
|
+
* `image/bmp` block is rejected there, and a rejected block costs the caller
|
|
23
|
+
* the metadata as well as the file rather than merely failing to draw.
|
|
24
|
+
*
|
|
25
|
+
* These four are therefore what is safe to inline. A host backed by something
|
|
26
|
+
* other than Claude may well accept more; nothing here asserts otherwise, it
|
|
27
|
+
* just does not rely on it.
|
|
28
|
+
*
|
|
29
|
+
* `image/svg+xml` is absent for an unrelated reason — an SVG can carry script,
|
|
30
|
+
* and it is text, so it goes down the text path below where it can be read
|
|
31
|
+
* rather than executed.
|
|
32
|
+
*
|
|
33
|
+
* Anything not here is returned as an embedded resource, which every client
|
|
34
|
+
* can hold.
|
|
35
|
+
*/
|
|
36
|
+
const INLINE_IMAGE_TYPES = new Set([
|
|
37
|
+
'image/gif',
|
|
38
|
+
'image/jpeg',
|
|
39
|
+
'image/png',
|
|
40
|
+
'image/webp',
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Types whose bytes are meant to be read rather than rendered.
|
|
44
|
+
*
|
|
45
|
+
* These are returned as `resource.text`. Handing a caller base64 of a CSV is
|
|
46
|
+
* the same as handing it nothing: the point of fetching a log or a spreadsheet
|
|
47
|
+
* is the content, and base64 has to be decoded by something that can already
|
|
48
|
+
* read the file.
|
|
49
|
+
*/
|
|
50
|
+
const TEXT_TYPES = new Set([
|
|
51
|
+
'application/json',
|
|
52
|
+
'application/xml',
|
|
53
|
+
'image/svg+xml',
|
|
54
|
+
'text/csv',
|
|
55
|
+
'text/html',
|
|
56
|
+
'text/plain',
|
|
57
|
+
]);
|
|
58
|
+
const getIssueAttachmentSchema = buildToolSchema((t) => ({
|
|
59
|
+
issueId: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe(t('TOOL_GET_ISSUE_ATTACHMENT_ISSUE_ID', 'The numeric ID of the issue (e.g., 12345)')),
|
|
64
|
+
issueKey: z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe(t('TOOL_GET_ISSUE_ATTACHMENT_ISSUE_KEY', "The key of the issue (e.g., 'PROJ-123')")),
|
|
68
|
+
attachmentId: z
|
|
69
|
+
.number()
|
|
70
|
+
.int()
|
|
71
|
+
.positive()
|
|
72
|
+
.describe(t('TOOL_GET_ISSUE_ATTACHMENT_ATTACHMENT_ID', "The numeric ID of the attachment. Get it from get_issue (the issue's `attachments` array).")),
|
|
73
|
+
format: z
|
|
74
|
+
.enum(['auto', 'base64'])
|
|
75
|
+
.optional()
|
|
76
|
+
.describe(t('TOOL_GET_ISSUE_ATTACHMENT_FORMAT', "How to return the file. 'auto' (default) returns a verified raster image as MCP image content and anything else as an embedded resource, so a client can render it. 'base64' returns a single JSON object with the encoded bytes in `content`, for callers that process the file programmatically — note that this puts the whole encoding in the caller's context.")),
|
|
77
|
+
maxBytes: z
|
|
78
|
+
.number()
|
|
79
|
+
.int()
|
|
80
|
+
.positive()
|
|
81
|
+
.max(MAX_ATTACHMENT_BYTES)
|
|
82
|
+
.optional()
|
|
83
|
+
.describe(t('TOOL_GET_ISSUE_ATTACHMENT_MAX_BYTES', `Maximum raw attachment size in bytes. Defaults to ${DEFAULT_MAX_BYTES} and cannot exceed ${MAX_ATTACHMENT_BYTES}.`)),
|
|
84
|
+
}));
|
|
85
|
+
function isReadableByteStream(body) {
|
|
86
|
+
return (typeof body === 'object' &&
|
|
87
|
+
body !== null &&
|
|
88
|
+
'getReader' in body &&
|
|
89
|
+
typeof body.getReader === 'function');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Reads the whole body, refusing to buffer more than `maxBytes`.
|
|
93
|
+
*
|
|
94
|
+
* The limit is checked per chunk rather than against a `Content-Length`:
|
|
95
|
+
* `backlog-js` does not surface response headers, and a header would be the
|
|
96
|
+
* server's claim rather than what actually arrived.
|
|
97
|
+
*/
|
|
98
|
+
async function readBody(body, maxBytes) {
|
|
99
|
+
if (!isReadableByteStream(body)) {
|
|
100
|
+
throw new Error('Backlog returned an unsupported attachment body.');
|
|
101
|
+
}
|
|
102
|
+
const reader = body.getReader();
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let size = 0;
|
|
105
|
+
let finished = false;
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done) {
|
|
110
|
+
finished = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (!(value instanceof Uint8Array)) {
|
|
114
|
+
throw new Error('Backlog returned a non-binary attachment chunk.');
|
|
115
|
+
}
|
|
116
|
+
size += value.byteLength;
|
|
117
|
+
if (size > maxBytes) {
|
|
118
|
+
throw new Error(`Attachment exceeds the ${maxBytes}-byte response limit. Raise maxBytes (up to ${MAX_ATTACHMENT_BYTES}) if the file really is this large.`);
|
|
119
|
+
}
|
|
120
|
+
chunks.push(value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
// Leaving the loop before the stream ends leaves the response in flight —
|
|
125
|
+
// `releaseLock` detaches the reader, it does not abort what is arriving.
|
|
126
|
+
// Cancelling here rather than at each `throw` covers every way out,
|
|
127
|
+
// including a `read()` that rejects, and cannot be forgotten by the next
|
|
128
|
+
// early return added above.
|
|
129
|
+
if (!finished) {
|
|
130
|
+
try {
|
|
131
|
+
await reader.cancel();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Best effort: keep whatever error got us here.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
reader.releaseLock();
|
|
138
|
+
}
|
|
139
|
+
const bytes = new Uint8Array(size);
|
|
140
|
+
let offset = 0;
|
|
141
|
+
for (const chunk of chunks) {
|
|
142
|
+
bytes.set(chunk, offset);
|
|
143
|
+
offset += chunk.byteLength;
|
|
144
|
+
}
|
|
145
|
+
return { bytes, size };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `btoa` over a string built in 32 KiB slices.
|
|
149
|
+
*
|
|
150
|
+
* `String.fromCharCode(...bytes)` on a multi-megabyte array overflows the call
|
|
151
|
+
* stack, and `Buffer` is not available to this module: `src/lib.ts` exposes the
|
|
152
|
+
* tool layer to consumers that do not run on Node.
|
|
153
|
+
*/
|
|
154
|
+
function toBase64(bytes) {
|
|
155
|
+
const chunks = [];
|
|
156
|
+
const chunkSize = 0x8000;
|
|
157
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
158
|
+
chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)));
|
|
159
|
+
}
|
|
160
|
+
return globalThis.btoa(chunks.join(''));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A name safe to put in metadata and a resource URI.
|
|
164
|
+
*
|
|
165
|
+
* `backlog-js` decodes `Content-Disposition` since 0.20.1, so what arrives is
|
|
166
|
+
* already the real name. What is left is that it is the server's string: strip
|
|
167
|
+
* any directory part and control characters rather than pass those on, and
|
|
168
|
+
* fall back to the id when nothing usable remains.
|
|
169
|
+
*/
|
|
170
|
+
function normalizeFilename(rawName, attachmentId) {
|
|
171
|
+
const basename = (rawName ?? '').split(/[/\\]/).pop() ?? '';
|
|
172
|
+
const cleaned = Array.from(basename)
|
|
173
|
+
.filter((character) => {
|
|
174
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
175
|
+
return codePoint > 0x1f && codePoint !== 0x7f;
|
|
176
|
+
})
|
|
177
|
+
.join('')
|
|
178
|
+
.trim();
|
|
179
|
+
return !cleaned || cleaned === '.' || cleaned === '..'
|
|
180
|
+
? `attachment-${attachmentId}`
|
|
181
|
+
: cleaned;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The bare MIME type of a raw `Content-Type` header.
|
|
185
|
+
*
|
|
186
|
+
* `File.FileData.contentType` is the header verbatim — `backlog-js` reads it
|
|
187
|
+
* with `response.headers.get('Content-Type')` and hands it over unparsed — so
|
|
188
|
+
* it may carry parameters and any casing. Every decision below is an exact
|
|
189
|
+
* lookup in `INLINE_IMAGE_TYPES` or `TEXT_TYPES`, and `text/csv;charset=UTF-8`
|
|
190
|
+
* misses both: the CSV would come back as base64, which is the case this tool
|
|
191
|
+
* exists to serve.
|
|
192
|
+
*/
|
|
193
|
+
function bareMimeType(rawContentType) {
|
|
194
|
+
return rawContentType.split(';')[0].trim().toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
function startsWith(bytes, signature) {
|
|
197
|
+
return signature.every((byte, index) => bytes[index] === byte);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether the bytes really are the raster format the extension promises.
|
|
201
|
+
*
|
|
202
|
+
* A client renders `image` content without checking it, so an attachment named
|
|
203
|
+
* `.png` that is not a PNG would otherwise become a broken image with no
|
|
204
|
+
* explanation. Mismatches fall back to an embedded resource.
|
|
205
|
+
*/
|
|
206
|
+
function isExpectedImage(bytes, contentType) {
|
|
207
|
+
switch (contentType) {
|
|
208
|
+
case 'image/gif':
|
|
209
|
+
return (startsWith(bytes, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
|
210
|
+
startsWith(bytes, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]));
|
|
211
|
+
case 'image/jpeg':
|
|
212
|
+
return startsWith(bytes, [0xff, 0xd8, 0xff]);
|
|
213
|
+
case 'image/png':
|
|
214
|
+
return startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
215
|
+
case 'image/webp':
|
|
216
|
+
return (startsWith(bytes, [0x52, 0x49, 0x46, 0x46]) &&
|
|
217
|
+
bytes[8] === 0x57 &&
|
|
218
|
+
bytes[9] === 0x45 &&
|
|
219
|
+
bytes[10] === 0x42 &&
|
|
220
|
+
bytes[11] === 0x50);
|
|
221
|
+
default:
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The raster type the bytes actually are, if it is one this tool inlines.
|
|
227
|
+
*
|
|
228
|
+
* A file saved under the wrong extension — a PNG named `.jpg` is the common
|
|
229
|
+
* one — is served happily by Backlog. Trusting the name and giving up would
|
|
230
|
+
* turn it into an opaque blob no client draws, when the bytes needed to
|
|
231
|
+
* identify it are already in hand.
|
|
232
|
+
*/
|
|
233
|
+
function sniffImageType(bytes) {
|
|
234
|
+
for (const type of INLINE_IMAGE_TYPES) {
|
|
235
|
+
if (isExpectedImage(bytes, type)) {
|
|
236
|
+
return type;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `undefined` rather than replacement characters when the bytes are not UTF-8,
|
|
243
|
+
* so a binary file under a `.txt` name falls back to a blob instead of
|
|
244
|
+
* arriving as mojibake that reads like a successful download.
|
|
245
|
+
*/
|
|
246
|
+
function decodeUtf8(bytes) {
|
|
247
|
+
try {
|
|
248
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function getSpaceNamespace(sourceUrl) {
|
|
255
|
+
try {
|
|
256
|
+
return new URL(sourceUrl).host || 'unknown-space';
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return 'unknown-space';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A synthetic `backlog://` URI, never the URL the download came from.
|
|
264
|
+
*
|
|
265
|
+
* In API-key mode `backlog-js` puts the key in the query string, and
|
|
266
|
+
* `File.FileData.url` carries it verbatim — echoing it into a resource URI
|
|
267
|
+
* would hand the caller a working credential.
|
|
268
|
+
*/
|
|
269
|
+
function buildResourceUri(sourceUrl, issueIdOrKey, attachmentId, filename) {
|
|
270
|
+
const space = encodeURIComponent(getSpaceNamespace(sourceUrl));
|
|
271
|
+
return `backlog://attachments/${space}/issues/${encodeURIComponent(String(issueIdOrKey))}/attachments/${attachmentId}/${encodeURIComponent(filename)}`;
|
|
272
|
+
}
|
|
273
|
+
function errorResult(message) {
|
|
274
|
+
return {
|
|
275
|
+
isError: true,
|
|
276
|
+
content: [{ type: 'text', text: message }],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export const getIssueAttachmentTool = (backlog, { t }) => {
|
|
280
|
+
return {
|
|
281
|
+
name: 'get_issue_attachment',
|
|
282
|
+
description: t('TOOL_GET_ISSUE_ATTACHMENT_DESCRIPTION', "Downloads one attachment of an issue. Returns it as MCP image or embedded resource content by default, or as base64 in a JSON object with `format: 'base64'`. Call get_issue first to obtain the attachmentId from the issue's `attachments` array."),
|
|
283
|
+
schema: z.object(getIssueAttachmentSchema(t)),
|
|
284
|
+
handler: async ({ issueId, issueKey, attachmentId, format, maxBytes }) => {
|
|
285
|
+
const resolved = resolveIdOrKey('issue', { id: issueId, key: issueKey }, t);
|
|
286
|
+
if (!resolved.ok) {
|
|
287
|
+
return errorResult(resolved.error.message);
|
|
288
|
+
}
|
|
289
|
+
const fileData = await backlog.getIssueAttachment(resolved.value, attachmentId);
|
|
290
|
+
const filename = normalizeFilename('filename' in fileData ? fileData.filename : undefined, attachmentId);
|
|
291
|
+
const { bytes, size } = await readBody(fileData.body, maxBytes ?? DEFAULT_MAX_BYTES);
|
|
292
|
+
// What the bytes are beats what the server says, because Backlog derives
|
|
293
|
+
// `Content-Type` from the extension too — an attachment saved as `.jpg`
|
|
294
|
+
// whose bytes are a PNG is served as `image/jpeg`. Only when nothing
|
|
295
|
+
// matches and the server promised a raster does the type become opaque:
|
|
296
|
+
// saying `image/png` for something that is not one makes a client draw a
|
|
297
|
+
// broken image with no explanation.
|
|
298
|
+
const declaredType = bareMimeType(fileData.contentType) || 'application/octet-stream';
|
|
299
|
+
const contentType = sniffImageType(bytes) ??
|
|
300
|
+
(INLINE_IMAGE_TYPES.has(declaredType)
|
|
301
|
+
? 'application/octet-stream'
|
|
302
|
+
: declaredType);
|
|
303
|
+
if (format === 'base64') {
|
|
304
|
+
const data = toBase64(bytes);
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: 'text',
|
|
309
|
+
text: JSON.stringify({ filename, contentType, size, content: data }, null, 2),
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const metadata = {
|
|
315
|
+
filename,
|
|
316
|
+
contentType,
|
|
317
|
+
size,
|
|
318
|
+
attachmentId,
|
|
319
|
+
issueIdOrKey: resolved.value,
|
|
320
|
+
};
|
|
321
|
+
if (INLINE_IMAGE_TYPES.has(contentType)) {
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{ type: 'text', text: JSON.stringify(metadata, null, 2) },
|
|
325
|
+
{ type: 'image', data: toBase64(bytes), mimeType: contentType },
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const uri = buildResourceUri(fileData.url, resolved.value, attachmentId, filename);
|
|
330
|
+
const text = TEXT_TYPES.has(contentType) ? decodeUtf8(bytes) : undefined;
|
|
331
|
+
const binaryContent = {
|
|
332
|
+
type: 'resource',
|
|
333
|
+
resource: text === undefined
|
|
334
|
+
? { uri, blob: toBase64(bytes), mimeType: contentType }
|
|
335
|
+
: { uri, text, mimeType: contentType },
|
|
336
|
+
};
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{ type: 'text', text: JSON.stringify(metadata, null, 2) },
|
|
340
|
+
binaryContent,
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DescriptionHelper } from '../createDescriptionHelper.js';
|
|
2
|
+
import { BacklogClientRegistry } from '../utils/backlogClientRegistry.js';
|
|
3
|
+
import { ToolDefinition } from '../types/tool.js';
|
|
4
|
+
import { ToolsetGroup } from '../types/toolsets.js';
|
|
5
|
+
type OrganizationOutput = {
|
|
6
|
+
name: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
isDefault: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function organizationTools(registry: BacklogClientRegistry, { t }: DescriptionHelper): ToolsetGroup;
|
|
11
|
+
export declare function listOrganizationsTool(registry: BacklogClientRegistry, t: DescriptionHelper['t']): ToolDefinition<Record<string, never>, OrganizationOutput>;
|
|
12
|
+
export {};
|
|
@@ -16,17 +16,17 @@ export function listOrganizationsTool(registry, t) {
|
|
|
16
16
|
name: 'list_organizations',
|
|
17
17
|
description: t('TOOL_LIST_ORGANIZATIONS_DESCRIPTION', 'List configured Backlog organizations and identify the default organization.'),
|
|
18
18
|
schema: z.object({}),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
outputFields: ['name', 'domain', 'isDefault'],
|
|
20
|
+
/**
|
|
21
|
+
* False even though the handler returns an array.
|
|
22
|
+
*
|
|
23
|
+
* `returnsList` decides whether `--optimize-response` publishes a `fields`
|
|
24
|
+
* parameter, and that pays off where a response grows without bound. This
|
|
25
|
+
* one is bounded by how many spaces the operator configured, over three
|
|
26
|
+
* fields — a `fields` enum would cost every client schema to trim nothing.
|
|
27
|
+
*/
|
|
28
|
+
returnsList: false,
|
|
29
|
+
handler: async () => registry.listOrganizations().map(toToolOutput),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
function toToolOutput(organization) {
|
package/build/tools/tools.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getCustomFieldsTool } from './getCustomFields.js';
|
|
|
13
13
|
import { getGitRepositoriesTool } from './getGitRepositories.js';
|
|
14
14
|
import { getGitRepositoryTool } from './getGitRepository.js';
|
|
15
15
|
import { getIssueTool } from './getIssue.js';
|
|
16
|
+
import { getIssueAttachmentTool } from './getIssueAttachment.js';
|
|
16
17
|
import { getIssueCommentsTool } from './getIssueComments.js';
|
|
17
18
|
import { getIssuesTool } from './getIssues.js';
|
|
18
19
|
import { getRelatedIssuesTool } from './getRelatedIssues.js';
|
|
@@ -93,6 +94,7 @@ export const allTools = (backlog, helper) => {
|
|
|
93
94
|
name: 'issue',
|
|
94
95
|
description: 'Tools for managing issues and their comments.',
|
|
95
96
|
enabled: false,
|
|
97
|
+
nativeContentTools: [getIssueAttachmentTool(backlog, helper)],
|
|
96
98
|
tools: [
|
|
97
99
|
getIssueTool(backlog, helper),
|
|
98
100
|
getIssuesTool(backlog, helper),
|
package/build/types/tool.d.ts
CHANGED
|
@@ -29,7 +29,25 @@ export type ToolDefinition<Shape extends z.ZodRawShape, Result> = {
|
|
|
29
29
|
returnsList: boolean;
|
|
30
30
|
};
|
|
31
31
|
export declare const buildToolSchema: <T extends z.ZodRawShape>(fn: (t: DescriptionHelper['t']) => T) => (t: DescriptionHelper['t']) => T;
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A tool that assembles its own `CallToolResult`.
|
|
34
|
+
*
|
|
35
|
+
* The exception, not a second way of writing a tool: a `ToolDefinition` returns
|
|
36
|
+
* a plain value and the handler pipeline turns it into a result, which is what
|
|
37
|
+
* almost every tool wants. This type exists for the few whose result the
|
|
38
|
+
* pipeline cannot express or would corrupt — `wrapWithToolResult` ends a tool at
|
|
39
|
+
* exactly one text block, so `image` and `resource` content is unreachable
|
|
40
|
+
* through it, and `wrapWithTokenLimit` would cut a base64 payload mid-string and
|
|
41
|
+
* return it as `kind: 'ok'`, a corrupt file reported as a success.
|
|
42
|
+
*
|
|
43
|
+
* The name is the content, not the tool: these produce MCP content types
|
|
44
|
+
* natively rather than being reshaped into one. What they give up is everything
|
|
45
|
+
* the pipeline does — field picking, the token limit, JSON serialisation — so
|
|
46
|
+
* reach for it only when the result shape actually requires it.
|
|
47
|
+
* `composeNativeContentToolHandler` puts back the two steps that are not about
|
|
48
|
+
* reshaping, the organization context and error handling.
|
|
49
|
+
*/
|
|
50
|
+
export type NativeContentToolDefinition<Shape extends z.ZodRawShape> = {
|
|
33
51
|
name: string;
|
|
34
52
|
description: string;
|
|
35
53
|
schema: z.ZodObject<Shape>;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NativeContentToolDefinition, ToolDefinition } from './tool.js';
|
|
2
2
|
type BaseToolset<TTool> = {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
5
|
enabled: boolean;
|
|
6
6
|
tools: TTool[];
|
|
7
7
|
};
|
|
8
|
-
export type Toolset = BaseToolset<ToolDefinition<any, any
|
|
8
|
+
export type Toolset = BaseToolset<ToolDefinition<any, any>> & {
|
|
9
|
+
/**
|
|
10
|
+
* Tools of this toolset that build their own `CallToolResult`.
|
|
11
|
+
*
|
|
12
|
+
* A separate field rather than a member of `tools`, because the two are
|
|
13
|
+
* registered through different pipelines: a `ToolDefinition` is reshaped by
|
|
14
|
+
* field picking and the token limit, which a tool returning binary content
|
|
15
|
+
* must not be. Keeping them in one toolset is what makes `--enable-toolsets`
|
|
16
|
+
* and the prefix apply to both.
|
|
17
|
+
*/
|
|
18
|
+
nativeContentTools?: NativeContentToolDefinition<any>[];
|
|
19
|
+
};
|
|
9
20
|
export type ToolsetGroup = {
|
|
10
21
|
toolsets: Toolset[];
|
|
11
22
|
};
|
|
12
|
-
export type DynamicToolset = BaseToolset<DynamicToolDefinition<any>>;
|
|
13
|
-
export type DynamicToolsetGroup = {
|
|
14
|
-
toolsets: DynamicToolset[];
|
|
15
|
-
};
|
|
16
23
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backlog-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"backlog-mcp-server": "./build/index.js"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@hono/node-server": "^2.1.1",
|
|
37
37
|
"@modelcontextprotocol/hono": "^2.0.0",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
39
|
-
"backlog-js": "^0.
|
|
39
|
+
"backlog-js": "^0.20.1",
|
|
40
40
|
"env-var": "^7.5.0",
|
|
41
41
|
"hono": "^4.13.5",
|
|
42
42
|
"js-yaml": "^5.4.1",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { DescriptionHelper } from '../../createDescriptionHelper.js';
|
|
2
|
-
import { BacklogClientRegistry } from '../../utils/backlogClientRegistry.js';
|
|
3
|
-
import { DynamicToolDefinition } from '../../types/tool.js';
|
|
4
|
-
import { DynamicToolsetGroup } from '../../types/toolsets.js';
|
|
5
|
-
export declare function organizationTools(registry: BacklogClientRegistry, { t }: DescriptionHelper): DynamicToolsetGroup;
|
|
6
|
-
export declare function listOrganizationsTool(registry: BacklogClientRegistry, t: DescriptionHelper['t']): DynamicToolDefinition<Record<string, never>>;
|