backlog-mcp-server 0.16.0 → 0.17.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/build/index.js CHANGED
@@ -11,6 +11,7 @@ import { createDescriptionHelper } from './createDescriptionHelper.js';
11
11
  import { loadDescriptionOverrides } from './loadDescriptionOverrides.js';
12
12
  import { createBacklogMcpServer } from './createBacklogMcpServer.js';
13
13
  import { runHttpMcpServer } from './httpMcpServer.js';
14
+ import { reportUnknownOverrideKeys } from './reportUnknownOverrideKeys.js';
14
15
  import { createBacklogClientRegistry, createOAuthBacklogClientRegistry, } from './utils/backlogClientRegistry.js';
15
16
  import { logger } from './utils/logger.js';
16
17
  import { buildToolsetGroup } from './utils/toolsetUtils.js';
@@ -144,7 +145,8 @@ if (tokenStore) {
144
145
  cleanupTimer.unref();
145
146
  }
146
147
  const useFields = argv.optimizeResponse;
147
- const descriptionHelper = createDescriptionHelper(loadDescriptionOverrides());
148
+ const descriptionOverrides = loadDescriptionOverrides();
149
+ const descriptionHelper = createDescriptionHelper(descriptionOverrides);
148
150
  const maxTokens = argv.maxTokens;
149
151
  const prefix = argv.prefix;
150
152
  const enabledToolsets = argv.dynamicToolsets
@@ -162,6 +164,13 @@ const mcpOption = {
162
164
  // Sharing it makes toolset state process-wide, which is the only scope left now
163
165
  // that the protocol has no sessions.
164
166
  const sharedToolsetGroup = buildToolsetGroup(backlog, descriptionHelper, enabledToolsets);
167
+ reportUnknownOverrideKeys({
168
+ overrides: descriptionOverrides,
169
+ version,
170
+ backlog,
171
+ clientRegistry,
172
+ mcpOption,
173
+ });
165
174
  // Factory: creates a fresh MCP server with all tools registered.
166
175
  // Used once per stdio connection; one fresh instance per HTTP request.
167
176
  const createServer = () => createBacklogMcpServer({
@@ -1,16 +1,3 @@
1
- /**
2
- * Reads description overrides from a `.backlog-mcp-serverrc` file (`.json`,
3
- * `.yaml` or `.yml`) in the user's home directory.
4
- *
5
- * Node-only, and kept separate from `createDescriptionHelper` for that reason:
6
- * cosmiconfig walks the filesystem and the default search path is the home
7
- * directory. The CLI calls this and hands the result to the helper.
8
- *
9
- * The file is user-authored, so its contents are unknown: anything that is not a
10
- * string is dropped here rather than passed on. Most overrides end up in a tool
11
- * description, and a number or an array there would produce an invalid
12
- * `tools/list` payload.
13
- */
14
1
  export declare function loadDescriptionOverrides(options?: {
15
2
  configName?: string;
16
3
  searchDir?: string;
@@ -1,24 +1,86 @@
1
- import { cosmiconfigSync } from 'cosmiconfig';
1
+ import { readFileSync } from 'fs';
2
+ import { load } from 'js-yaml';
2
3
  import os from 'os';
4
+ import path from 'path';
5
+ import { logger } from './utils/logger.js';
3
6
  /**
4
- * Reads description overrides from a `.backlog-mcp-serverrc` file (`.json`,
5
- * `.yaml` or `.yml`) in the user's home directory.
7
+ * Reads description overrides from a `.backlog-mcp-serverrc` file in the user's
8
+ * home directory.
6
9
  *
7
10
  * Node-only, and kept separate from `createDescriptionHelper` for that reason:
8
- * cosmiconfig walks the filesystem and the default search path is the home
9
- * directory. The CLI calls this and hands the result to the helper.
11
+ * this reads the filesystem and needs a home directory. The CLI calls it and
12
+ * hands the result to the helper.
10
13
  *
11
14
  * The file is user-authored, so its contents are unknown: anything that is not a
12
15
  * string is dropped here rather than passed on. Most overrides end up in a tool
13
16
  * description, and a number or an array there would produce an invalid
14
17
  * `tools/list` payload.
18
+ *
19
+ * A file that exists but cannot be parsed is reported and then ignored.
20
+ * Overriding descriptions is an add-on, and a trailing comma in an optional file
21
+ * is not a reason to stop serving the tools — from the client's side an
22
+ * exception here looks like "the MCP server will not connect", with nothing
23
+ * pointing at the config file.
24
+ */
25
+ /**
26
+ * Checked in this order; the first one that exists wins. The extensionless form
27
+ * is parsed as YAML, which also accepts JSON.
28
+ */
29
+ const SUFFIXES = ['', '.json', '.yaml', '.yml'];
30
+ /**
31
+ * A missing candidate is not a failure — there are four of them and at most one
32
+ * exists. A candidate that exists but does not parse is, and has to be
33
+ * distinguished from the missing case so it can be reported rather than skipped.
15
34
  */
35
+ function readCandidate(filePath) {
36
+ let raw;
37
+ try {
38
+ raw = readFileSync(filePath, 'utf-8');
39
+ }
40
+ catch (error) {
41
+ if (error instanceof Error &&
42
+ 'code' in error &&
43
+ (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
44
+ return { status: 'absent' };
45
+ }
46
+ return { status: 'unreadable', error };
47
+ }
48
+ try {
49
+ // An empty file is a config file with nothing in it, not a parse failure.
50
+ // `JSON.parse('')` throws, so it never reaches the caller's object check.
51
+ if (raw.trim() === '')
52
+ return { status: 'found', config: undefined };
53
+ return {
54
+ status: 'found',
55
+ config: filePath.endsWith('.json') ? JSON.parse(raw) : load(raw),
56
+ };
57
+ }
58
+ catch (error) {
59
+ return { status: 'unreadable', error };
60
+ }
61
+ }
16
62
  export function loadDescriptionOverrides(options) {
17
- const explorer = cosmiconfigSync(options?.configName ?? 'backlog-mcp-server');
18
- const searchPath = options?.searchDir ?? os.homedir();
19
- const config = explorer.search(searchPath)?.config;
20
- if (typeof config !== 'object' || config === null || Array.isArray(config)) {
21
- return {};
63
+ const configName = options?.configName ?? 'backlog-mcp-server';
64
+ const searchDir = options?.searchDir ?? os.homedir();
65
+ for (const suffix of SUFFIXES) {
66
+ const filePath = path.join(searchDir, `.${configName}rc${suffix}`);
67
+ const outcome = readCandidate(filePath);
68
+ if (outcome.status === 'absent')
69
+ continue;
70
+ if (outcome.status === 'unreadable') {
71
+ // `logger.error`, not `warn`: the logger runs at level `error` unless
72
+ // NODE_ENV says otherwise, so a warning here would never reach the user
73
+ // this message exists for.
74
+ logger.error({ err: outcome.error, filePath }, 'Could not read the description override file; continuing with the built-in defaults');
75
+ return {};
76
+ }
77
+ const config = outcome.config;
78
+ if (typeof config !== 'object' ||
79
+ config === null ||
80
+ Array.isArray(config)) {
81
+ return {};
82
+ }
83
+ return Object.fromEntries(Object.entries(config).filter(([, value]) => typeof value === 'string'));
22
84
  }
23
- return Object.fromEntries(Object.entries(config).filter(([, value]) => typeof value === 'string'));
85
+ return {};
24
86
  }
@@ -0,0 +1,22 @@
1
+ import type { Backlog } from 'backlog-js';
2
+ import type { MCPOptions } from './types/mcp.js';
3
+ import type { BacklogClientRegistry } from './utils/backlogClientRegistry.js';
4
+ /**
5
+ * Reports override keys in the config file that no tool or parameter asks for.
6
+ *
7
+ * The override keys are an untyped, unversioned public API: rename one in the
8
+ * source and every user's override for it stops applying, falls back to the
9
+ * built-in default, and says nothing. The user sees "I configured this and it
10
+ * has no effect". This turns that into a line in the server log.
11
+ *
12
+ * The comparison is exact, which also catches keys written in the wrong case —
13
+ * `t()` looks up `key.toUpperCase()`, so a lower-case entry in the config file
14
+ * never matches anything either.
15
+ */
16
+ export declare function reportUnknownOverrideKeys({ overrides, version, backlog, clientRegistry, mcpOption, }: {
17
+ overrides: Record<string, string>;
18
+ version: string;
19
+ backlog: Backlog;
20
+ clientRegistry: BacklogClientRegistry;
21
+ mcpOption: MCPOptions;
22
+ }): void;
@@ -0,0 +1,43 @@
1
+ import { createBacklogMcpServer } from './createBacklogMcpServer.js';
2
+ import { createDescriptionHelper } from './createDescriptionHelper.js';
3
+ import { logger } from './utils/logger.js';
4
+ /**
5
+ * Reports override keys in the config file that no tool or parameter asks for.
6
+ *
7
+ * The override keys are an untyped, unversioned public API: rename one in the
8
+ * source and every user's override for it stops applying, falls back to the
9
+ * built-in default, and says nothing. The user sees "I configured this and it
10
+ * has no effect". This turns that into a line in the server log.
11
+ *
12
+ * The comparison is exact, which also catches keys written in the wrong case —
13
+ * `t()` looks up `key.toUpperCase()`, so a lower-case entry in the config file
14
+ * never matches anything either.
15
+ */
16
+ export function reportUnknownOverrideKeys({ overrides, version, backlog, clientRegistry, mcpOption, }) {
17
+ const configured = Object.keys(overrides);
18
+ if (configured.length === 0)
19
+ return;
20
+ // Keys are only recorded once a tool asks for them, so the set of valid keys
21
+ // is whatever building the full tool list touches. This is a throwaway helper
22
+ // and server built purely to collect them: the real ones may have toolsets
23
+ // disabled, and their keys would then look unknown. ~17ms, and only for users
24
+ // who actually have a config file.
25
+ const probe = createDescriptionHelper();
26
+ createBacklogMcpServer({
27
+ version,
28
+ useFields: mcpOption.useFields,
29
+ backlog,
30
+ clientRegistry,
31
+ descriptionHelper: probe,
32
+ enabledToolsets: ['all'],
33
+ mcpOption: { ...mcpOption, useOrganization: true },
34
+ dynamicToolsets: true,
35
+ });
36
+ const known = new Set(Object.keys(probe.dump()));
37
+ const unknown = configured.filter((key) => !known.has(key));
38
+ if (unknown.length === 0)
39
+ return;
40
+ // `error`, not `warn`: the logger drops anything below error in the default
41
+ // configuration, which is what users run.
42
+ logger.error({ keys: unknown }, 'These description override keys match no tool or parameter and had no effect');
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backlog-mcp-server",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "backlog-mcp-server": "./build/index.js"
@@ -37,17 +37,18 @@
37
37
  "@modelcontextprotocol/hono": "^2.0.0",
38
38
  "@modelcontextprotocol/server": "^2.0.0",
39
39
  "backlog-js": "^0.19.1",
40
- "cosmiconfig": "^9.0.1",
41
40
  "env-var": "^7.5.0",
42
- "graphql": "^16.14.1",
41
+ "graphql": "^17.0.2",
43
42
  "hono": "^4.12.34",
43
+ "js-yaml": "^5.2.3",
44
44
  "pino": "^10.3.1",
45
45
  "yargs": "^18.0.0",
46
46
  "zod": "^4.4.3"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@eslint/js": "^10.0.1",
50
- "@types/node": "^25.9.2",
50
+ "@types/js-yaml": "^4.0.9",
51
+ "@types/node": "^26.2.0",
51
52
  "@types/yargs": "^17.0.35",
52
53
  "@typescript-eslint/eslint-plugin": "^8.60.1",
53
54
  "@typescript-eslint/parser": "^8.60.1",