backlog-mcp-server 0.19.0 → 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.
@@ -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 { registerDynamicTools, registerTools } from './registerTools.js';
5
- import { organizationTools } from './tools/dynamicTools/organizations.js';
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
- registerDynamicTools(server, organizationTools(clientRegistry, descriptionHelper), mcpOption.prefix);
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
  }
@@ -1,11 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { ErrorLike } from '../../types/result.js';
3
- import { DynamicToolDefinition } from '../../types/tool.js';
4
- export type ComposeDynamicOptions = {
3
+ import { NativeContentToolDefinition } from '../../types/tool.js';
4
+ export type ComposeNativeContentOptions = {
5
5
  errorHandler?: (err: unknown) => ErrorLike;
6
6
  useOrganization?: boolean;
7
7
  };
8
- type DynamicInput = {
8
+ type NativeContentInput = {
9
9
  organization?: string;
10
10
  } & Record<string, unknown>;
11
11
  /**
@@ -31,7 +31,7 @@ type DynamicInput = {
31
31
  * definition is never mutated, because one toolset group is shared across
32
32
  * per-request servers.
33
33
  */
34
- export declare function composeDynamicToolHandler(tool: DynamicToolDefinition<any>, { errorHandler, useOrganization }?: ComposeDynamicOptions): {
34
+ export declare function composeNativeContentToolHandler(tool: NativeContentToolDefinition<any>, { errorHandler, useOrganization }?: ComposeNativeContentOptions): {
35
35
  schema: z.ZodObject<{
36
36
  [x: string]: any;
37
37
  organization: z.ZodOptional<z.ZodString>;
@@ -39,7 +39,7 @@ export declare function composeDynamicToolHandler(tool: DynamicToolDefinition<an
39
39
  [x: string]: any;
40
40
  organization?: undefined;
41
41
  }, z.core.$strip>;
42
- handler: (input: DynamicInput) => Promise<{
42
+ handler: (input: NativeContentInput) => Promise<{
43
43
  [x: string]: unknown;
44
44
  _meta?: {
45
45
  [x: string]: unknown;
@@ -25,7 +25,7 @@ import { isErrorLike } from '../../types/result.js';
25
25
  * definition is never mutated, because one toolset group is shared across
26
26
  * per-request servers.
27
27
  */
28
- export function composeDynamicToolHandler(
28
+ export function composeNativeContentToolHandler(
29
29
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
30
  tool, { errorHandler, useOrganization = false } = {}) {
31
31
  // `extend` even with nothing to add, so the returned schema is always a copy.
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,14 +14,14 @@
14
14
  */
15
15
  export { allTools } from './tools/tools.js';
16
16
  export { composeToolHandler } from './handlers/builders/composeToolHandler.js';
17
- export { composeDynamicToolHandler } from './handlers/builders/composeDynamicToolHandler.js';
17
+ export { composeNativeContentToolHandler } from './handlers/builders/composeNativeContentToolHandler.js';
18
18
  export { createDescriptionHelper } from './createDescriptionHelper.js';
19
19
  export { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
20
20
  export { buildToolSchema } from './types/tool.js';
21
21
  export { isErrorLike } from './types/result.js';
22
22
  export type { ComposeOptions } from './handlers/builders/composeToolHandler.js';
23
- export type { ComposeDynamicOptions } from './handlers/builders/composeDynamicToolHandler.js';
23
+ export type { ComposeNativeContentOptions } from './handlers/builders/composeNativeContentToolHandler.js';
24
24
  export type { DescriptionHelper } from './createDescriptionHelper.js';
25
- export type { ToolDefinition, DynamicToolDefinition } from './types/tool.js';
26
- export type { Toolset, ToolsetGroup, DynamicToolset, DynamicToolsetGroup, } from './types/toolsets.js';
25
+ export type { ToolDefinition, NativeContentToolDefinition, } from './types/tool.js';
26
+ export type { Toolset, ToolsetGroup } from './types/toolsets.js';
27
27
  export type { ErrorLike, SafeResult } from './types/result.js';
package/build/lib.js CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
  export { allTools } from './tools/tools.js';
16
16
  export { composeToolHandler } from './handlers/builders/composeToolHandler.js';
17
- export { composeDynamicToolHandler } from './handlers/builders/composeDynamicToolHandler.js';
17
+ export { composeNativeContentToolHandler } from './handlers/builders/composeNativeContentToolHandler.js';
18
18
  export { createDescriptionHelper } from './createDescriptionHelper.js';
19
19
  export { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
20
20
  export { buildToolSchema } from './types/tool.js';
@@ -1,5 +1,4 @@
1
1
  import { MCPOptions } from './types/mcp.js';
2
- import { DynamicToolsetGroup, ToolsetGroup } from './types/toolsets.js';
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;
@@ -1,15 +1,13 @@
1
1
  import { backlogErrorHandler } from './backlog/backlogErrorHandler.js';
2
- import { composeDynamicToolHandler } from './handlers/builders/composeDynamicToolHandler.js';
2
+ import { composeNativeContentToolHandler } from './handlers/builders/composeNativeContentToolHandler.js';
3
3
  import { composeToolHandler } from './handlers/builders/composeToolHandler.js';
4
4
  export function registerTools(server, toolsetGroup, options) {
5
5
  const { useFields, maxTokens, prefix, useOrganization } = options;
6
6
  registerToolsets({
7
7
  server,
8
- toolsetGroup,
8
+ toolsets: toolsetGroup.toolsets,
9
9
  prefix,
10
- prepareTool: (tool) =>
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- composeToolHandler(tool, {
10
+ prepareTool: (tool) => composeToolHandler(tool, {
13
11
  useFields,
14
12
  errorHandler: backlogErrorHandler,
15
13
  maxTokens,
@@ -20,30 +18,19 @@ export function registerTools(server, toolsetGroup, options) {
20
18
  // that `--enable-toolsets` and the prefix cover them too.
21
19
  registerToolsets({
22
20
  server,
23
- toolsetGroup: {
24
- toolsets: toolsetGroup.toolsets.map((toolset) => ({
25
- name: toolset.name,
26
- description: toolset.description,
27
- enabled: toolset.enabled,
28
- tools: toolset.dynamicTools ?? [],
29
- })),
30
- },
21
+ toolsets: toolsetGroup.toolsets.map((toolset) => ({
22
+ enabled: toolset.enabled,
23
+ tools: toolset.nativeContentTools ?? [],
24
+ })),
31
25
  prefix,
32
- prepareTool: (tool) => composeDynamicToolHandler(
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- tool, { errorHandler: backlogErrorHandler, useOrganization }),
35
- });
36
- }
37
- export function registerDynamicTools(server, dynamicToolsetGroup, prefix) {
38
- registerToolsets({
39
- server,
40
- toolsetGroup: dynamicToolsetGroup,
41
- prefix,
42
- prepareTool: (tool) => ({ schema: tool.schema, handler: tool.handler }),
26
+ prepareTool: (tool) => composeNativeContentToolHandler(tool, {
27
+ errorHandler: backlogErrorHandler,
28
+ useOrganization,
29
+ }),
43
30
  });
44
31
  }
45
- function registerToolsets({ server, toolsetGroup, prefix, prepareTool, }) {
46
- for (const toolset of toolsetGroup.toolsets) {
32
+ function registerToolsets({ server, toolsets, prefix, prepareTool, }) {
33
+ for (const toolset of toolsets) {
47
34
  if (!toolset.enabled) {
48
35
  continue;
49
36
  }
@@ -1,7 +1,7 @@
1
1
  import { Backlog } from 'backlog-js';
2
2
  import { z } from 'zod';
3
3
  import { DescriptionHelper } from '../createDescriptionHelper.js';
4
- import { DynamicToolDefinition } from '../types/tool.js';
4
+ import { NativeContentToolDefinition } from '../types/tool.js';
5
5
  declare const getIssueAttachmentSchema: (t: DescriptionHelper['t']) => {
6
6
  issueId: z.ZodOptional<z.ZodNumber>;
7
7
  issueKey: z.ZodOptional<z.ZodString>;
@@ -12,5 +12,5 @@ declare const getIssueAttachmentSchema: (t: DescriptionHelper['t']) => {
12
12
  }>>;
13
13
  maxBytes: z.ZodOptional<z.ZodNumber>;
14
14
  };
15
- export declare const getIssueAttachmentTool: (backlog: Backlog, { t }: DescriptionHelper) => DynamicToolDefinition<ReturnType<typeof getIssueAttachmentSchema>>;
15
+ export declare const getIssueAttachmentTool: (backlog: Backlog, { t }: DescriptionHelper) => NativeContentToolDefinition<ReturnType<typeof getIssueAttachmentSchema>>;
16
16
  export {};
@@ -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
- 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
- },
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) {
@@ -94,7 +94,7 @@ export const allTools = (backlog, helper) => {
94
94
  name: 'issue',
95
95
  description: 'Tools for managing issues and their comments.',
96
96
  enabled: false,
97
- dynamicTools: [getIssueAttachmentTool(backlog, helper)],
97
+ nativeContentTools: [getIssueAttachmentTool(backlog, helper)],
98
98
  tools: [
99
99
  getIssueTool(backlog, helper),
100
100
  getIssuesTool(backlog, helper),
@@ -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
- export type DynamicToolDefinition<Shape extends z.ZodRawShape> = {
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,4 +1,4 @@
1
- import { DynamicToolDefinition, ToolDefinition } from './tool.js';
1
+ import { NativeContentToolDefinition, ToolDefinition } from './tool.js';
2
2
  type BaseToolset<TTool> = {
3
3
  name: string;
4
4
  description: string;
@@ -15,13 +15,9 @@ export type Toolset = BaseToolset<ToolDefinition<any, any>> & {
15
15
  * must not be. Keeping them in one toolset is what makes `--enable-toolsets`
16
16
  * and the prefix apply to both.
17
17
  */
18
- dynamicTools?: DynamicToolDefinition<any>[];
18
+ nativeContentTools?: NativeContentToolDefinition<any>[];
19
19
  };
20
20
  export type ToolsetGroup = {
21
21
  toolsets: Toolset[];
22
22
  };
23
- export type DynamicToolset = BaseToolset<DynamicToolDefinition<any>>;
24
- export type DynamicToolsetGroup = {
25
- toolsets: DynamicToolset[];
26
- };
27
23
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "backlog-mcp-server": "./build/index.js"
@@ -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>>;