@desktopaccountingapi/quickbooks-desktop-mcp 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - `list_api_endpoints` finds the searches its description suggests: "profit and loss" and "open invoices" now return the report operation and name the matching `reportType` (for example `profit_and_loss_standard`, `open_invoices`). Words match as tokens against the operation name, resource, summary, description, path, parameter names and report types; case, plurals and stop words do not matter, common synonyms count (P&L, A/R, supplier for vendor), and results come best match first. An exact operation or tool name (`qbd.salesOrders.list`, `qbd_sales_orders_list`, `generalSummary`) is found and listed first.
6
+
3
7
  ## 0.1.0
4
8
 
5
- First release of `@desktopaccountingapi/quickbooks-desktop-mcp`, generated from API contract sha256 `68a0d76d6b51` (API version 1.0.0, 275 operations).
9
+ First release of `@desktopaccountingapi/quickbooks-desktop-mcp`, generated from API contract sha256 `1fc5496cc47b` (API version 1.0.0, 275 operations).
6
10
 
7
11
  - Local MCP server over stdio: `npx -y @desktopaccountingapi/quickbooks-desktop-mcp`. Node.js 20 or later on Windows, macOS and Linux; no other runtime and no runtime dependencies.
8
12
  - Tools: `list_end_users`, `list_api_endpoints`, `get_api_endpoint_schema`, `invoke_api_endpoint`, `search_docs`; optional one tool per operation with `--resources`.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  - Writes carry an idempotency key and are never retried blindly. Read-only keys are enforced by the API itself.
7
7
  - Runs over stdio with Node.js 20 or later on Windows, macOS and Linux, with no runtime dependencies.
8
8
 
9
- The current version is **0.4.0**. [MCP guide](https://www.desktopaccountingapi.com/docs/guides/mcp/) · [Documentation](https://www.desktopaccountingapi.com/docs/) · [Changelog](CHANGELOG.md) · [Status](https://status.desktopaccountingapi.com)
9
+ The current version is **0.5.0**. [MCP guide](https://www.desktopaccountingapi.com/docs/guides/mcp/) · [Documentation](https://www.desktopaccountingapi.com/docs/) · [Changelog](CHANGELOG.md) · [Status](https://status.desktopaccountingapi.com)
10
10
 
11
11
  ## Hosted server or local package
12
12
 
@@ -37,14 +37,14 @@ Open **Settings > Developer > Edit Config** (`claude_desktop_config.json`) and a
37
37
  "mcpServers": {
38
38
  "quickbooks-desktop": {
39
39
  "command": "npx",
40
- "args": ["-y", "@desktopaccountingapi/quickbooks-desktop-mcp@0.4.0"],
40
+ "args": ["-y", "@desktopaccountingapi/quickbooks-desktop-mcp@0.5.0"],
41
41
  "env": { "DAAPI_SECRET_KEY": "sk_live_..." }
42
42
  }
43
43
  }
44
44
  }
45
45
  ```
46
46
 
47
- If the file already has an `mcpServers` section, add the `quickbooks-desktop` entry inside it, then restart Claude Desktop. Drop `@0.4.0` from the package name to always run the latest version.
47
+ If the file already has an `mcpServers` section, add the `quickbooks-desktop` entry inside it, then restart Claude Desktop. Drop `@0.5.0` from the package name to always run the latest version.
48
48
 
49
49
  ### Claude Code
50
50
 
@@ -125,7 +125,7 @@ We recommend a **read-only** secret key for AI tools. The API rejects every writ
125
125
  | Tool | What it does |
126
126
  | --- | --- |
127
127
  | `list_end_users` | Your end users (one QuickBooks company file each), with connection status and QuickBooks company name. |
128
- | `list_api_endpoints` | Searches the operations by resource, name or words, for example "open invoices" or "profit and loss". |
128
+ | `list_api_endpoints` | Searches the operations by resource, name, words or report type, for example "open invoices" or "profit and loss", and names the matching report types. |
129
129
  | `get_api_endpoint_schema` | One operation's description and the JSON Schema of its arguments. |
130
130
  | `invoke_api_endpoint` | Calls an operation and returns its JSON result. `fields` (dot paths such as `["id", "refNumber"]`) trims each record. |
131
131
  | `search_docs` | Searches the documentation, including the error codes. |
@@ -173,7 +173,7 @@ Clients send their own secret key as `Authorization: Bearer sk_...`; the server
173
173
  ## Versioning and changelog
174
174
 
175
175
  - The package follows [semantic versioning](https://semver.org/) and is released together with the [Node.js](https://github.com/DesktopAccountingAPI/quickbooks-desktop-node), [Python](https://github.com/DesktopAccountingAPI/quickbooks-desktop-python), [.NET](https://github.com/DesktopAccountingAPI/quickbooks-desktop-dotnet) and [Java](https://github.com/DesktopAccountingAPI/quickbooks-desktop-java) SDKs, with the same version number.
176
- - It is generated from the Desktop Accounting API contract (sha256 `68a0d76d6b51...` for this release) by the same pipeline as the SDKs.
176
+ - It is generated from the Desktop Accounting API contract (sha256 `1fc5496cc47b...` for this release) by the same pipeline as the SDKs.
177
177
  - Every release is listed in [CHANGELOG.md](CHANGELOG.md) and tagged `v<version>` on GitHub.
178
178
 
179
179
  ## Support
package/dist/catalog.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // MCP endpoint catalog: a compact, input-only view of the public OpenAPI contract that the MCP
4
4
  // tools search, describe and invoke. Built from packages/api-contract/generated/openapi.json by
5
5
  // scripts/api-contract.mjs (committed as packages/mcp/generated/catalog.json, drift-checked) and
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
3
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
3
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
4
4
  // Local (stdio) MCP server for the Desktop Accounting API.
5
5
  //
6
6
  // npx -y @desktopaccountingapi/quickbooks-desktop-mcp [--read-only] [--resources invoices,customers] [--end-user-id eu_...]
package/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // Streamable HTTP transport (MCP 2025-03-26 and later), stateless: each POST carries one JSON-RPC
4
4
  // message (or a batch, for 2025-03-26 clients) and is answered with application/json. No server
5
5
  // sessions and no server-initiated stream, so GET and DELETE return 405. Web-standard Request and
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // Runtime-agnostic MCP server core (no Node-only imports); the tool design is in tools.ts.
4
4
  export { buildCatalog, defsFor } from './catalog.js';
5
5
  export { handleMcpRequest } from './http.js';
package/dist/key.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // Local secret-key check (same rule as the API and SDKs, docs/api-conventions.md section 2):
4
4
  // `sk_live_`/`sk_test_` + 40 base62 chars, the last 6 being the base62 CRC32 of the first 34.
5
5
  // Rejecting a mistyped key locally keeps it from counting against the API's per-IP
@@ -0,0 +1,32 @@
1
+ import type { Catalog, CatalogEndpoint } from './catalog.ts';
2
+ /** Lowercase stemmed tokens; splits camelCase, snake_case, kebab-case, dots and slashes. */
3
+ export declare function tokens(text: string): string[];
4
+ /** The words of a search query, without stop words, each with the words it also matches. */
5
+ export declare function queryWords(search: string): string[][];
6
+ interface Field {
7
+ weight: number;
8
+ tokens: Set<string>;
9
+ }
10
+ export interface SearchEntry {
11
+ endpoint: CatalogEndpoint;
12
+ fields: Field[];
13
+ /** Values of required enum query parameters, for example reportType values. */
14
+ selectors: {
15
+ param: string;
16
+ value: string;
17
+ tokens: Set<string>;
18
+ }[];
19
+ }
20
+ export declare function searchEntry(catalog: Catalog, e: CatalogEndpoint): SearchEntry;
21
+ export interface SearchHit {
22
+ endpoint: CatalogEndpoint;
23
+ score: number;
24
+ /** Selector values (for example reportType `open_invoices`) that contain every query word, by parameter. */
25
+ selectors: {
26
+ param: string;
27
+ values: string[];
28
+ }[];
29
+ }
30
+ /** Scores one operation against the query words; null when a word matches nothing. */
31
+ export declare function scoreEntry(entry: SearchEntry, words: string[][]): SearchHit | null;
32
+ export {};
package/dist/search.js ADDED
@@ -0,0 +1,112 @@
1
+ // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
+ // Operation search for list_api_endpoints. Words are matched as tokens (case, plurals and
4
+ // camelCase/snake_case ignored, a word of 5+ letters also matches the start of a longer one)
5
+ // against an operation's name, tag, summary, description, path, query parameter names and the
6
+ // values of its required selector parameters (for example the report families' `reportType`:
7
+ // `profit_and_loss_standard`, `open_invoices`). Every word must match; common accounting synonyms
8
+ // count as the same word. Each result names the selector values that matched, so "profit and
9
+ // loss" leads straight to qbd.reports.generalSummary with reportType profit_and_loss_standard.
10
+ const STOP_WORDS = new Set(['a', 'an', 'and', 'the', 'of', 'for', 'by', 'to', 'in', 'on', 'with', 'or', 'from', 'my', 'our', 'all']);
11
+ /** Phrases rewritten before tokenizing. */
12
+ const PHRASES = [
13
+ [/\bp\s*&\s*l\b|\bpnl\b|\bincome statement\b/gi, 'profit loss'],
14
+ [/\ba\s*\/\s*r\b/gi, 'ar'],
15
+ [/\ba\s*\/\s*p\b/gi, 'ap'],
16
+ ];
17
+ /** A query word also matches these words (already stemmed). */
18
+ const SYNONYMS = {
19
+ receivable: ['ar'],
20
+ ar: ['receivable'],
21
+ payable: ['ap'],
22
+ ap: ['payable'],
23
+ unpaid: ['open'],
24
+ outstanding: ['open'],
25
+ overdue: ['aging', 'open'],
26
+ supplier: ['vendor'],
27
+ client: ['customer'],
28
+ product: ['item'],
29
+ service: ['item'],
30
+ staff: ['employee'],
31
+ };
32
+ function stem(word) {
33
+ if (word.length > 4 && word.endsWith('ies'))
34
+ return word.slice(0, -3) + 'y';
35
+ if (word.length > 3 && word.endsWith('s') && !word.endsWith('ss'))
36
+ return word.slice(0, -1);
37
+ return word;
38
+ }
39
+ /** Lowercase stemmed tokens; splits camelCase, snake_case, kebab-case, dots and slashes. */
40
+ export function tokens(text) {
41
+ return text
42
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
43
+ .toLowerCase()
44
+ .split(/[^a-z0-9]+/)
45
+ .filter(Boolean)
46
+ .map(stem);
47
+ }
48
+ /** The words of a search query, without stop words, each with the words it also matches. */
49
+ export function queryWords(search) {
50
+ // Phrases are rewritten case-insensitively, but the query keeps its case until tokens() so a
51
+ // camelCase name (qbd.salesOrders.list, generalSummary) splits exactly like the index does.
52
+ let q = search;
53
+ for (const [re, to] of PHRASES)
54
+ q = q.replace(re, to);
55
+ return tokens(q)
56
+ .filter((w) => !STOP_WORDS.has(w))
57
+ .map((w) => [w, ...(SYNONYMS[w] ?? [])]);
58
+ }
59
+ function enumOf(catalog, schema) {
60
+ const ref = typeof schema.$ref === 'string' ? schema.$ref.split('/').pop() : undefined;
61
+ const target = ref ? catalog.defs[ref] : schema;
62
+ return Array.isArray(target?.enum) ? target.enum.filter((v) => typeof v === 'string') : [];
63
+ }
64
+ export function searchEntry(catalog, e) {
65
+ const field = (weight, ...texts) => ({ weight, tokens: new Set(texts.flatMap(tokens)) });
66
+ const selectors = e.queryParams
67
+ .filter((p) => p.required)
68
+ .flatMap((p) => enumOf(catalog, p.schema).map((value) => ({ param: p.name, value, tokens: new Set(tokens(value)) })));
69
+ return {
70
+ endpoint: e,
71
+ fields: [
72
+ field(3, e.name, e.tag),
73
+ field(2, e.summary),
74
+ { weight: 2, tokens: new Set(selectors.flatMap((s) => [...s.tokens])) },
75
+ field(1, e.group, e.path, e.description, ...e.queryParams.map((p) => p.name)),
76
+ ],
77
+ selectors,
78
+ };
79
+ }
80
+ const matches = (have, word) => {
81
+ if (have.has(word))
82
+ return true;
83
+ if (word.length < 5)
84
+ return false;
85
+ for (const t of have)
86
+ if (t.startsWith(word))
87
+ return true;
88
+ return false;
89
+ };
90
+ const matchesAny = (have, alternatives) => alternatives.some((w) => matches(have, w));
91
+ /** Scores one operation against the query words; null when a word matches nothing. */
92
+ export function scoreEntry(entry, words) {
93
+ let score = 0;
94
+ for (const alternatives of words) {
95
+ let best = 0;
96
+ for (const f of entry.fields)
97
+ if (f.weight > best && matchesAny(f.tokens, alternatives))
98
+ best = f.weight;
99
+ if (!best)
100
+ return null;
101
+ score += best;
102
+ }
103
+ const full = words.length ? entry.selectors.filter((s) => words.every((alts) => matchesAny(s.tokens, alts))) : [];
104
+ // A selector value holding the whole multi-word query ("profit and loss") outranks scattered
105
+ // matches, but not an operation whose name holds every word ("sales orders": qbd.salesOrders.*).
106
+ if (full.length && words.length > 1)
107
+ score += 1;
108
+ const byParam = new Map();
109
+ for (const s of full)
110
+ byParam.set(s.param, [...(byParam.get(s.param) ?? []), s.value]);
111
+ return { endpoint: entry.endpoint, score, selectors: [...byParam].map(([param, values]) => ({ param, values })) };
112
+ }
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // Model Context Protocol server core: JSON-RPC 2.0 message handling for the `tools` capability.
4
4
  // Transport-independent; stdio.ts and http.ts carry the messages. Implements the lifecycle
5
5
  // (initialize with version negotiation, notifications/initialized, ping) plus tools/list and
package/dist/stdio.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // stdio transport: newline-delimited JSON-RPC on stdin/stdout (MCP specification, "stdio").
4
4
  // Only protocol messages go to stdout; diagnostics go to stderr. Pure Node.js (no Deno, no
5
5
  // POSIX-only features), so it runs the same on Windows, macOS and Linux.
package/dist/tools.d.ts CHANGED
@@ -64,6 +64,7 @@ export declare class Tools {
64
64
  opts: ToolOptions;
65
65
  private readonly byName;
66
66
  private readonly perEndpoint;
67
+ private readonly searchIndex;
67
68
  private docsCache;
68
69
  private prepared;
69
70
  constructor(opts: ToolOptions);
@@ -81,6 +82,7 @@ export declare class Tools {
81
82
  call(name: string, args?: Record<string, unknown>): Promise<ToolResult>;
82
83
  private resolve;
83
84
  private listEndpoints;
85
+ private searchEntry;
84
86
  private endpointSchema;
85
87
  private headers;
86
88
  private keyProblem;
package/dist/tools.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // Code generated by packages/sdk-generator from packages/mcp/src. DO NOT EDIT.
2
- // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:68a0d76d6b51187adfc66d768eae8e30675b5093f4ae538b0d9c32c905b9bb2d
2
+ // Contract: packages/api-contract/generated/openapi.json (OpenAPI 3.1.0) sha256:1fc5496cc47b442606ea78c818f94a29798b32785dc14c59e7986e24f10bf332
3
3
  // MCP tools over the Desktop Accounting API. Runtime-agnostic (fetch and Web Crypto only), so the
4
4
  // same code runs in the hosted Cloudflare Worker (apps/mcp) and the stdio npm package.
5
5
  //
6
6
  // Default tool set (dynamic, so 275 operations do not flood the client's context):
7
7
  // list_end_users the project's end users and their connection status
8
- // list_api_endpoints search the operations by name, resource or text
8
+ // list_api_endpoints search the operations by name, resource, text or report type (search.ts)
9
9
  // get_api_endpoint_schema one operation's description and input schema
10
10
  // invoke_api_endpoint call one operation
11
11
  // search_docs search the documentation (llms-full.txt)
@@ -20,6 +20,7 @@
20
20
  // found in it (prompt-injection boundary).
21
21
  import { defsFor } from './catalog.js';
22
22
  import { isValidSecretKey, maskKey } from './key.js';
23
+ import { queryWords, scoreEntry, searchEntry } from './search.js';
23
24
  export const SERVER_NAME = 'desktopaccountingapi-quickbooks-desktop';
24
25
  const END_USER_ID = { type: 'string', pattern: '^eu_[0-9a-hjkmnp-tv-z]{26}$', description: 'The end user (eu_...) whose QuickBooks company file to use. Get it from list_end_users. Overrides the connection default.' };
25
26
  const IDEMPOTENCY_KEY = { type: 'string', minLength: 1, maxLength: 255, description: 'Writes only. Leave empty to get a fresh key. To retry a write whose outcome is unknown, send the key from the earlier attempt: the API then attaches to or replays the original instead of writing twice.' };
@@ -93,6 +94,7 @@ export class Tools {
93
94
  opts;
94
95
  byName = new Map();
95
96
  perEndpoint = new Map();
97
+ searchIndex = new Map();
96
98
  docsCache;
97
99
  prepared;
98
100
  constructor(opts) {
@@ -163,7 +165,7 @@ export class Tools {
163
165
  inputSchema: {
164
166
  type: 'object',
165
167
  properties: {
166
- search: { type: 'string', description: 'Words to match in the operation name, resource, summary or path, for example "invoice", "open invoices", "profit and loss".' },
168
+ search: { type: 'string', description: 'Words that must all match the operation name, resource, summary, description, path, parameter names or report type, for example "invoice", "open invoices", "profit and loss", "ar aging". Case and plurals do not matter, and common synonyms (supplier for vendor, P&L for profit and loss) count as the same word. Results come best match first and name the matching reportType.' },
167
169
  group: { type: 'string', description: `Only this group: ${groups}.` },
168
170
  kind: { type: 'string', enum: ['read', 'write', 'all'], description: 'Only reads (GET), only writes, or all (default).' },
169
171
  },
@@ -254,7 +256,8 @@ export class Tools {
254
256
  return `Unknown endpoint "${endpoint}".${close.length ? ` Did you mean: ${close.join(', ')}?` : ' Use list_api_endpoints to find it.'}`;
255
257
  }
256
258
  listEndpoints(args) {
257
- const words = String(args.search ?? '').toLowerCase().split(/\s+/).filter(Boolean).map((w) => w.replace(/s$/, ''));
259
+ const words = queryWords(String(args.search ?? ''));
260
+ const exact = String(args.search ?? '').trim().toLowerCase();
258
261
  const group = args.group ? slug(String(args.group)) : '';
259
262
  const kind = args.kind === 'read' || args.kind === 'write' ? args.kind : 'all';
260
263
  const rows = [];
@@ -265,23 +268,32 @@ export class Tools {
265
268
  continue;
266
269
  if (kind === 'write' && !e.write)
267
270
  continue;
268
- const hay = `${e.name} ${e.tag} ${e.summary} ${e.path}`.toLowerCase();
269
- let score = 0;
270
- for (const w of words) {
271
- if (!hay.includes(w)) {
272
- score = -1;
273
- break;
274
- }
275
- score += e.name.toLowerCase().includes(w) || e.tag.toLowerCase().includes(w) ? 2 : 1;
276
- }
277
- if (score >= 0)
278
- rows.push({ e, score });
271
+ const hit = scoreEntry(this.searchEntry(e), words);
272
+ if (!hit)
273
+ continue;
274
+ // The exact operation or tool name always comes first.
275
+ if (exact && (exact === e.name.toLowerCase() || exact === toolName(e.name)))
276
+ hit.score += 100;
277
+ rows.push(hit);
279
278
  }
280
- rows.sort((a, b) => b.score - a.score);
279
+ // Best score first; on a tie the operation name with fewer words (qbd.customers.* before qbd.customerTypes.*).
280
+ const size = (h) => h.endpoint.name.split(/\.|(?=[A-Z])/).length;
281
+ rows.sort((a, b) => b.score - a.score || size(a) - size(b));
281
282
  if (!rows.length)
282
283
  return text(`No operation matches${words.length ? ` "${String(args.search)}"` : ''}. Try fewer or broader words, or omit search to list every operation.`);
283
- const lines = rows.map(({ e }) => `${e.name} ${e.method} ${e.path} ${e.summary}${e.write ? ' [write]' : ''}${e.endUser ? '' : ' [no end user]'}`);
284
- return text(`${rows.length} operation${rows.length === 1 ? '' : 's'} (name, method, path, summary). QuickBooks operations need end_user_id unless marked [no end user].\n${lines.join('\n')}`);
284
+ const lines = rows.map(({ endpoint: e, selectors }) => {
285
+ const shown = selectors.map(({ param, values }) => `${param} ${values.slice(0, 6).join(', ')}${values.length > 6 ? ` and ${values.length - 6} more` : ''}`).join('; ');
286
+ return `${e.name} ${e.method} ${e.path} ${e.summary}${e.write ? ' [write]' : ''}${e.endUser ? '' : ' [no end user]'}${shown ? ` (matching ${shown})` : ''}`;
287
+ });
288
+ return text(`${rows.length} operation${rows.length === 1 ? '' : 's'} (name, method, path, summary), best match first. QuickBooks operations need end_user_id unless marked [no end user].\n${lines.join('\n')}`);
289
+ }
290
+ searchEntry(e) {
291
+ let entry = this.searchIndex.get(e.name);
292
+ if (!entry) {
293
+ entry = searchEntry(this.opts.catalog, e);
294
+ this.searchIndex.set(e.name, entry);
295
+ }
296
+ return entry;
285
297
  }
286
298
  endpointSchema(args) {
287
299
  const e = this.resolve(args.endpoint);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desktopaccountingapi/quickbooks-desktop-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Model Context Protocol (MCP) server for Desktop Accounting API: QuickBooks Desktop for Claude, Cursor, VS Code, Codex and other AI tools.",
5
5
  "license": "MIT",
6
6
  "author": "Desktop Accounting API",