@mindstone-engineering/mcp-server-quickbooks 0.1.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/dist/auth.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * QuickBooks Online authentication module.
3
+ *
4
+ * OAuth2 refresh token grant with token rotation.
5
+ * Credentials managed via env vars or configured at runtime.
6
+ * Supports sandbox vs production environment switching.
7
+ *
8
+ * Environment variables:
9
+ * - QUICKBOOKS_CLIENT_ID: Intuit Developer app Client ID
10
+ * - QUICKBOOKS_CLIENT_SECRET: Intuit Developer app Client Secret
11
+ * - QUICKBOOKS_REFRESH_TOKEN: OAuth2 refresh token
12
+ * - QUICKBOOKS_REALM_ID: QuickBooks company ID (Realm ID)
13
+ * - QUICKBOOKS_ENVIRONMENT: "sandbox" or "production" (default: "production")
14
+ */
15
+ export declare function getClientId(): string;
16
+ export declare function setClientId(id: string): void;
17
+ export declare function getClientSecret(): string;
18
+ export declare function setClientSecret(s: string): void;
19
+ export declare function getRefreshToken(): string;
20
+ export declare function setRefreshToken(t: string): void;
21
+ export declare function getRealmId(): string;
22
+ export declare function setRealmId(id: string): void;
23
+ export declare function getEnvironment(): string;
24
+ export declare function setEnvironment(env: string): void;
25
+ export declare function clearTokenCache(): void;
26
+ export declare function isConfigured(): boolean;
27
+ export declare function getApiHost(): string;
28
+ export declare function getBaseUrl(): string;
29
+ export declare function validateEnvironment(env: string): boolean;
30
+ export declare function getAccessToken(): Promise<string>;
31
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,141 @@
1
+ /**
2
+ * QuickBooks Online authentication module.
3
+ *
4
+ * OAuth2 refresh token grant with token rotation.
5
+ * Credentials managed via env vars or configured at runtime.
6
+ * Supports sandbox vs production environment switching.
7
+ *
8
+ * Environment variables:
9
+ * - QUICKBOOKS_CLIENT_ID: Intuit Developer app Client ID
10
+ * - QUICKBOOKS_CLIENT_SECRET: Intuit Developer app Client Secret
11
+ * - QUICKBOOKS_REFRESH_TOKEN: OAuth2 refresh token
12
+ * - QUICKBOOKS_REALM_ID: QuickBooks company ID (Realm ID)
13
+ * - QUICKBOOKS_ENVIRONMENT: "sandbox" or "production" (default: "production")
14
+ */
15
+ import { QuickBooksError, TOKEN_URL, USER_AGENT, REQUEST_TIMEOUT_MS } from './types.js';
16
+ import { bridgeRequest } from './bridge.js';
17
+ // ── Runtime credentials ──
18
+ let clientId = process.env.QUICKBOOKS_CLIENT_ID ?? '';
19
+ let clientSecret = process.env.QUICKBOOKS_CLIENT_SECRET ?? '';
20
+ let refreshToken = process.env.QUICKBOOKS_REFRESH_TOKEN ?? '';
21
+ let realmId = process.env.QUICKBOOKS_REALM_ID ?? '';
22
+ let environment = (() => {
23
+ const envVal = process.env.QUICKBOOKS_ENVIRONMENT ?? 'production';
24
+ if (envVal !== 'sandbox' && envVal !== 'production') {
25
+ throw new QuickBooksError(`Invalid QUICKBOOKS_ENVIRONMENT: "${envVal}". Must be "sandbox" or "production".`, 'INVALID_CONFIG', 'Set QUICKBOOKS_ENVIRONMENT to either "sandbox" or "production".');
26
+ }
27
+ return envVal;
28
+ })();
29
+ // ── Token cache ──
30
+ let cachedAccessToken = null;
31
+ let tokenExpiresAt = 0;
32
+ // ── Getters / setters ──
33
+ export function getClientId() {
34
+ return clientId;
35
+ }
36
+ export function setClientId(id) {
37
+ clientId = id;
38
+ }
39
+ export function getClientSecret() {
40
+ return clientSecret;
41
+ }
42
+ export function setClientSecret(s) {
43
+ clientSecret = s;
44
+ }
45
+ export function getRefreshToken() {
46
+ return refreshToken;
47
+ }
48
+ export function setRefreshToken(t) {
49
+ refreshToken = t;
50
+ }
51
+ export function getRealmId() {
52
+ return realmId;
53
+ }
54
+ export function setRealmId(id) {
55
+ realmId = id;
56
+ }
57
+ export function getEnvironment() {
58
+ return environment;
59
+ }
60
+ export function setEnvironment(env) {
61
+ environment = env;
62
+ }
63
+ export function clearTokenCache() {
64
+ cachedAccessToken = null;
65
+ tokenExpiresAt = 0;
66
+ }
67
+ export function isConfigured() {
68
+ return !!(clientId && clientSecret && refreshToken && realmId);
69
+ }
70
+ // ── Environment / URL helpers ──
71
+ export function getApiHost() {
72
+ return environment === 'sandbox'
73
+ ? 'sandbox-quickbooks.api.intuit.com'
74
+ : 'quickbooks.api.intuit.com';
75
+ }
76
+ export function getBaseUrl() {
77
+ return `https://${getApiHost()}/v3/company/${realmId}`;
78
+ }
79
+ export function validateEnvironment(env) {
80
+ return env === 'sandbox' || env === 'production';
81
+ }
82
+ // ── Token exchange ──
83
+ export async function getAccessToken() {
84
+ if (!clientId || !clientSecret || !refreshToken) {
85
+ throw new QuickBooksError('QuickBooks not configured. Call configure_quickbooks first.', 'NOT_CONFIGURED', 'Configure QuickBooks with your Intuit Developer app credentials first.');
86
+ }
87
+ if (cachedAccessToken && Date.now() < tokenExpiresAt) {
88
+ return cachedAccessToken;
89
+ }
90
+ const authHeader = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
91
+ const body = new URLSearchParams({
92
+ grant_type: 'refresh_token',
93
+ refresh_token: refreshToken,
94
+ });
95
+ let response;
96
+ try {
97
+ response = await fetch(TOKEN_URL, {
98
+ method: 'POST',
99
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
100
+ headers: {
101
+ Authorization: authHeader,
102
+ 'Content-Type': 'application/x-www-form-urlencoded',
103
+ Accept: 'application/json',
104
+ 'User-Agent': USER_AGENT,
105
+ },
106
+ body: body.toString(),
107
+ });
108
+ }
109
+ catch (error) {
110
+ if (error instanceof Error && error.name === 'TimeoutError') {
111
+ throw new QuickBooksError('OAuth token request timed out', 'TIMEOUT', 'The request took too long. Check your network connectivity.');
112
+ }
113
+ throw error;
114
+ }
115
+ if (!response.ok) {
116
+ let errorText;
117
+ try {
118
+ const errorBody = await response.json();
119
+ errorText = errorBody?.error_description || errorBody?.error || JSON.stringify(errorBody);
120
+ }
121
+ catch {
122
+ errorText = await response.text().catch(() => 'Unknown error');
123
+ }
124
+ throw new QuickBooksError(`OAuth token refresh failed (${response.status}): ${errorText}`, 'AUTH_FAILED', 'Re-configure with configure_quickbooks. Check Client ID, Secret, and Refresh Token. Tokens expire after 100 days of inactivity.');
125
+ }
126
+ const tokenData = await response.json();
127
+ cachedAccessToken = tokenData.access_token;
128
+ // Expire 60 seconds early to avoid edge cases
129
+ tokenExpiresAt = Date.now() + (tokenData.expires_in - 60) * 1000;
130
+ // Handle refresh token rotation — Intuit may return a new refresh token
131
+ if (tokenData.refresh_token && tokenData.refresh_token !== refreshToken) {
132
+ refreshToken = tokenData.refresh_token;
133
+ bridgeRequest('/bundled/quickbooks/update-refresh-token', {
134
+ refreshToken: tokenData.refresh_token,
135
+ }).catch((err) => {
136
+ console.error('Failed to persist rotated refresh token:', err instanceof Error ? err.message : String(err));
137
+ });
138
+ }
139
+ return cachedAccessToken;
140
+ }
141
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Path to bridge state file, supporting both current and legacy env vars.
3
+ */
4
+ export declare const BRIDGE_STATE_PATH: string;
5
+ /**
6
+ * Send a request to the host app bridge.
7
+ *
8
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
9
+ * that handles credential management and other cross-process operations.
10
+ */
11
+ export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
12
+ success: boolean;
13
+ warning?: string;
14
+ error?: string;
15
+ }>;
16
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,43 @@
1
+ import * as fs from 'fs';
2
+ import { REQUEST_TIMEOUT_MS } from './types.js';
3
+ /**
4
+ * Path to bridge state file, supporting both current and legacy env vars.
5
+ */
6
+ export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
7
+ const loadBridgeState = () => {
8
+ if (!BRIDGE_STATE_PATH)
9
+ return null;
10
+ try {
11
+ const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ };
18
+ /**
19
+ * Send a request to the host app bridge.
20
+ *
21
+ * The bridge is an HTTP server running inside the host app (e.g. Rebel)
22
+ * that handles credential management and other cross-process operations.
23
+ */
24
+ export const bridgeRequest = async (urlPath, body) => {
25
+ const bridge = loadBridgeState();
26
+ if (!bridge) {
27
+ return { success: false, error: 'Bridge not available' };
28
+ }
29
+ const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
30
+ method: 'POST',
31
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${bridge.token}`,
35
+ },
36
+ body: JSON.stringify(body),
37
+ });
38
+ if (response.status === 401 || response.status === 403) {
39
+ return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
40
+ }
41
+ return response.json();
42
+ };
43
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * QuickBooks Online API HTTP client.
3
+ *
4
+ * Centralises Bearer auth injection, error handling,
5
+ * rate-limit retry with exponential backoff, and timeout handling.
6
+ *
7
+ * Auth: OAuth2 Bearer token via getAccessToken()
8
+ * Base URL: https://{host}/v3/company/{realmId}
9
+ */
10
+ /**
11
+ * Make an authenticated JSON request to the QuickBooks Online API.
12
+ */
13
+ export declare function qboFetch<T>(entityPath: string, options?: RequestInit, retryCount?: number): Promise<T>;
14
+ /**
15
+ * Run a QuickBooks query using the Query Language.
16
+ */
17
+ export declare function qboQuery<T>(entity: string, query: string, limit?: number, offset?: number): Promise<T[]>;
18
+ //# sourceMappingURL=client.d.ts.map
package/dist/client.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * QuickBooks Online API HTTP client.
3
+ *
4
+ * Centralises Bearer auth injection, error handling,
5
+ * rate-limit retry with exponential backoff, and timeout handling.
6
+ *
7
+ * Auth: OAuth2 Bearer token via getAccessToken()
8
+ * Base URL: https://{host}/v3/company/{realmId}
9
+ */
10
+ import { QuickBooksError, USER_AGENT, REQUEST_TIMEOUT_MS } from './types.js';
11
+ import { getAccessToken, getBaseUrl, isConfigured, clearTokenCache } from './auth.js';
12
+ /**
13
+ * Make an authenticated JSON request to the QuickBooks Online API.
14
+ */
15
+ export async function qboFetch(entityPath, options = {}, retryCount = 0) {
16
+ if (!isConfigured()) {
17
+ throw new QuickBooksError('QuickBooks not configured. Call configure_quickbooks first.', 'NOT_CONFIGURED', 'Configure QuickBooks with your Intuit Developer app credentials first.');
18
+ }
19
+ const accessToken = await getAccessToken();
20
+ const url = `${getBaseUrl()}${entityPath}`;
21
+ const headers = {
22
+ Authorization: `Bearer ${accessToken}`,
23
+ Accept: 'application/json',
24
+ 'Content-Type': 'application/json',
25
+ 'User-Agent': USER_AGENT,
26
+ ...options.headers,
27
+ };
28
+ console.error(`[QuickBooks API] ${options.method || 'GET'} ${url}`);
29
+ let response;
30
+ try {
31
+ response = await fetch(url, {
32
+ ...options,
33
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
34
+ headers,
35
+ });
36
+ }
37
+ catch (error) {
38
+ if (error instanceof Error && error.name === 'TimeoutError') {
39
+ throw new QuickBooksError('Request to QuickBooks API timed out', 'TIMEOUT', 'The request took too long. Try again or check if the QuickBooks API is available.');
40
+ }
41
+ throw error;
42
+ }
43
+ if (!response.ok) {
44
+ // Handle 429 with exponential backoff (max 3 retries)
45
+ if (response.status === 429 && retryCount < 3) {
46
+ const retryAfter = response.headers.get('Retry-After');
47
+ const waitMs = retryAfter
48
+ ? parseInt(retryAfter, 10) * 1000
49
+ : Math.min(1000 * Math.pow(2, retryCount), 8000);
50
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
51
+ return qboFetch(entityPath, options, retryCount + 1);
52
+ }
53
+ let errorText;
54
+ try {
55
+ const errorBody = await response.json();
56
+ const firstError = errorBody?.Fault?.Error?.[0];
57
+ errorText = firstError?.Detail || firstError?.Message || JSON.stringify(errorBody);
58
+ }
59
+ catch {
60
+ errorText = await response.text().catch(() => 'Unknown error');
61
+ }
62
+ if (response.status === 401) {
63
+ clearTokenCache();
64
+ throw new QuickBooksError(`Authentication failed (${response.status}): ${errorText}`, 'AUTH_FAILED', 'Re-configure with configure_quickbooks. Check your credentials.');
65
+ }
66
+ if (response.status === 403) {
67
+ throw new QuickBooksError(`Insufficient permissions (${response.status}): ${errorText}`, 'FORBIDDEN', 'Check your QuickBooks app permissions.');
68
+ }
69
+ if (response.status === 404) {
70
+ throw new QuickBooksError(`Resource not found (${response.status}): ${errorText}`, 'NOT_FOUND', 'Verify the entity type and ID are correct.');
71
+ }
72
+ if (response.status === 429) {
73
+ throw new QuickBooksError('Rate limited. Maximum retries exhausted.', 'RATE_LIMITED', 'Please wait before retrying.');
74
+ }
75
+ if (response.status >= 500) {
76
+ throw new QuickBooksError(`QuickBooks server error (${response.status}): ${errorText}`, 'SERVER_ERROR', 'QuickBooks server error. Try again later.');
77
+ }
78
+ throw new QuickBooksError(`QuickBooks API error (${response.status}): ${errorText}`, `HTTP_${response.status}`, 'Check the request parameters and try again.');
79
+ }
80
+ return await response.json();
81
+ }
82
+ /**
83
+ * Run a QuickBooks query using the Query Language.
84
+ */
85
+ export async function qboQuery(entity, query, limit = 100, offset = 1) {
86
+ const fullQuery = `${query} MAXRESULTS ${limit} STARTPOSITION ${offset}`;
87
+ const encoded = encodeURIComponent(fullQuery);
88
+ const result = await qboFetch(`/query?query=${encoded}`);
89
+ return result.QueryResponse[entity] || [];
90
+ }
91
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * QuickBooks Online MCP Server
4
+ *
5
+ * Provides QuickBooks Online accounting integration via Model Context Protocol.
6
+ * Covers invoices, bills, customers, vendors, employees, estimates, purchases,
7
+ * accounts, items, journal entries, and bill payments.
8
+ *
9
+ * Uses the QuickBooks Online REST API v3 directly via fetch().
10
+ * OAuth2 token refresh with rotation is handled internally.
11
+ *
12
+ * Environment variables:
13
+ * - QUICKBOOKS_CLIENT_ID: Intuit Developer app Client ID
14
+ * - QUICKBOOKS_CLIENT_SECRET: Intuit Developer app Client Secret
15
+ * - QUICKBOOKS_REFRESH_TOKEN: OAuth2 refresh token
16
+ * - QUICKBOOKS_REALM_ID: QuickBooks company ID (Realm ID)
17
+ * - QUICKBOOKS_ENVIRONMENT: "sandbox" or "production" (default: "production")
18
+ * - MCP_HOST_BRIDGE_STATE: Path to bridge state file for app communication
19
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path
20
+ */
21
+ export {};
22
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * QuickBooks Online MCP Server
4
+ *
5
+ * Provides QuickBooks Online accounting integration via Model Context Protocol.
6
+ * Covers invoices, bills, customers, vendors, employees, estimates, purchases,
7
+ * accounts, items, journal entries, and bill payments.
8
+ *
9
+ * Uses the QuickBooks Online REST API v3 directly via fetch().
10
+ * OAuth2 token refresh with rotation is handled internally.
11
+ *
12
+ * Environment variables:
13
+ * - QUICKBOOKS_CLIENT_ID: Intuit Developer app Client ID
14
+ * - QUICKBOOKS_CLIENT_SECRET: Intuit Developer app Client Secret
15
+ * - QUICKBOOKS_REFRESH_TOKEN: OAuth2 refresh token
16
+ * - QUICKBOOKS_REALM_ID: QuickBooks company ID (Realm ID)
17
+ * - QUICKBOOKS_ENVIRONMENT: "sandbox" or "production" (default: "production")
18
+ * - MCP_HOST_BRIDGE_STATE: Path to bridge state file for app communication
19
+ * - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path
20
+ */
21
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
+ import { createServer } from './server.js';
23
+ async function main() {
24
+ const server = createServer();
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ console.error('QuickBooks Online MCP server running on stdio');
28
+ }
29
+ main().catch((error) => {
30
+ console.error('Fatal error:', error);
31
+ process.exit(1);
32
+ });
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,18 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerConfigureTools, registerQueryTools, registerInvoiceTools, registerCustomerTools, registerBillTools, registerVendorTools, registerAccountTools, registerEmployeeTools, } from './tools/index.js';
3
+ export function createServer() {
4
+ const server = new McpServer({
5
+ name: 'quickbooks-mcp-server',
6
+ version: '0.1.0',
7
+ });
8
+ registerConfigureTools(server);
9
+ registerQueryTools(server);
10
+ registerInvoiceTools(server);
11
+ registerCustomerTools(server);
12
+ registerBillTools(server);
13
+ registerVendorTools(server);
14
+ registerAccountTools(server);
15
+ registerEmployeeTools(server);
16
+ return server;
17
+ }
18
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks account (chart of accounts) tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerAccountTools(server: McpServer): void;
6
+ //# sourceMappingURL=accounts.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * QuickBooks account (chart of accounts) tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboQuery } from '../client.js';
7
+ export function registerAccountTools(server) {
8
+ server.registerTool('list_quickbooks_accounts', {
9
+ description: `List chart of accounts from QuickBooks Online.
10
+
11
+ Returns: Id, Name, AccountType, AccountSubType, CurrentBalance, Active.
12
+
13
+ Example: {}
14
+ Example: { "accountType": "Expense" }
15
+
16
+ Account types: Bank, Accounts Receivable, Other Current Asset, Fixed Asset, Other Asset, Accounts Payable, Credit Card, Other Current Liability, Long Term Liability, Equity, Income, Cost of Goods Sold, Expense, Other Income, Other Expense.`,
17
+ inputSchema: z.object({
18
+ accountType: z.string().optional().describe('Filter by account type (e.g., "Expense", "Income", "Bank")'),
19
+ active: z.boolean().optional().describe('Filter by active status'),
20
+ limit: z.number().optional().describe('Max results (default: 100)'),
21
+ }),
22
+ annotations: { readOnlyHint: true },
23
+ }, withErrorHandling(async (args) => {
24
+ const limit = Math.min(args.limit ?? 100, 1000);
25
+ const conditions = [];
26
+ if (args.accountType)
27
+ conditions.push(`AccountType = '${args.accountType}'`);
28
+ if (args.active !== undefined)
29
+ conditions.push(`Active = ${args.active}`);
30
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
31
+ const query = `SELECT * FROM Account${where} ORDERBY Name`;
32
+ const accounts = await qboQuery('Account', query, limit);
33
+ return JSON.stringify({ ok: true, accounts, count: accounts.length });
34
+ }));
35
+ }
36
+ //# sourceMappingURL=accounts.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks bill tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerBillTools(server: McpServer): void;
6
+ //# sourceMappingURL=bills.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * QuickBooks bill tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboFetch, qboQuery } from '../client.js';
7
+ export function registerBillTools(server) {
8
+ server.registerTool('list_quickbooks_bills', {
9
+ description: `List bills (accounts payable) from QuickBooks Online.
10
+
11
+ Returns: Id, DocNumber, TxnDate, DueDate, Balance, TotalAmt, VendorRef.
12
+
13
+ Example: {}
14
+ Example: { "vendorId": "123" }`,
15
+ inputSchema: z.object({
16
+ vendorId: z.string().optional().describe('Filter by vendor ID'),
17
+ limit: z.number().optional().describe('Max results (default: 50)'),
18
+ }),
19
+ annotations: { readOnlyHint: true },
20
+ }, withErrorHandling(async (args) => {
21
+ const limit = Math.min(args.limit ?? 50, 1000);
22
+ const where = args.vendorId ? ` WHERE VendorRef = '${args.vendorId}'` : '';
23
+ const query = `SELECT * FROM Bill${where} ORDERBY TxnDate DESC`;
24
+ const bills = await qboQuery('Bill', query, limit);
25
+ return JSON.stringify({ ok: true, bills, count: bills.length });
26
+ }));
27
+ server.registerTool('create_quickbooks_bill', {
28
+ description: `Create a new bill (accounts payable) in QuickBooks Online.
29
+
30
+ Example: { "vendorId": "123", "lines": [{ "description": "Office supplies", "amount": 250, "accountId": "456" }] }
31
+
32
+ WORKFLOW:
33
+ 1. Use list_quickbooks_vendors to find the vendor ID
34
+ 2. Use list_quickbooks_accounts to find expense account IDs
35
+ 3. Create with line items`,
36
+ inputSchema: z.object({
37
+ vendorId: z.string().describe('Vendor ID (required)'),
38
+ lines: z.array(z.object({
39
+ description: z.string().describe('Line description'),
40
+ amount: z.number().describe('Line amount'),
41
+ accountId: z.string().optional().describe('Expense account ID'),
42
+ })).describe('Bill line items'),
43
+ dueDate: z.string().optional().describe('Due date (YYYY-MM-DD)'),
44
+ memo: z.string().optional().describe('Memo / notes'),
45
+ }),
46
+ annotations: { readOnlyHint: false, destructiveHint: false },
47
+ }, withErrorHandling(async (args) => {
48
+ const billBody = {
49
+ VendorRef: { value: args.vendorId },
50
+ Line: args.lines.map((line) => ({
51
+ Amount: line.amount,
52
+ DetailType: 'AccountBasedExpenseLineDetail',
53
+ Description: line.description,
54
+ AccountBasedExpenseLineDetail: {
55
+ ...(line.accountId ? { AccountRef: { value: line.accountId } } : {}),
56
+ },
57
+ })),
58
+ };
59
+ if (args.dueDate)
60
+ billBody.DueDate = args.dueDate;
61
+ if (args.memo)
62
+ billBody.PrivateNote = args.memo;
63
+ const result = await qboFetch('/bill?minorversion=65', { method: 'POST', body: JSON.stringify(billBody) });
64
+ return JSON.stringify({ ok: true, message: 'Bill created.', bill: result.Bill });
65
+ }));
66
+ }
67
+ //# sourceMappingURL=bills.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * QuickBooks credential configuration tool.
3
+ *
4
+ * Validates credentials via token exchange, persists via bridge,
5
+ * and updates runtime credentials.
6
+ */
7
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ export declare function registerConfigureTools(server: McpServer): void;
9
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * QuickBooks credential configuration tool.
3
+ *
4
+ * Validates credentials via token exchange, persists via bridge,
5
+ * and updates runtime credentials.
6
+ */
7
+ import { z } from 'zod';
8
+ import { TOKEN_URL, USER_AGENT, REQUEST_TIMEOUT_MS } from '../types.js';
9
+ import { withErrorHandling } from '../utils.js';
10
+ import { setClientId, setClientSecret, setRefreshToken, setRealmId, setEnvironment, clearTokenCache, validateEnvironment, } from '../auth.js';
11
+ import { bridgeRequest } from '../bridge.js';
12
+ export function registerConfigureTools(server) {
13
+ server.registerTool('configure_quickbooks', {
14
+ description: `Configure QuickBooks Online credentials. Call this when the user provides their Intuit Developer app credentials.
15
+
16
+ WORKFLOW:
17
+ 1. Go to https://developer.intuit.com/ and create an app (or use existing)
18
+ 2. Get the Client ID and Client Secret from the app's Keys & credentials page
19
+ 3. Add http://localhost:8000/callback as a Redirect URI
20
+ 4. Use the OAuth Playground or your app's auth flow to obtain a Refresh Token
21
+ 5. Find your Company ID (Realm ID) in the URL when logged into QuickBooks Online
22
+
23
+ COMMON MISTAKES:
24
+ - Refresh tokens expire after 100 days of inactivity — re-authenticate if you get auth errors
25
+ - The Realm ID is NOT the same as the Client ID — it's your company identifier
26
+ - Sandbox and Production use different credentials`,
27
+ inputSchema: z.object({
28
+ clientId: z.string().describe('Intuit Developer app Client ID'),
29
+ clientSecret: z.string().describe('Intuit Developer app Client Secret'),
30
+ refreshToken: z.string().describe('OAuth2 refresh token from the authorization flow'),
31
+ realmId: z.string().describe('QuickBooks company ID (Realm ID)'),
32
+ environment: z.enum(['sandbox', 'production']).optional().default('production')
33
+ .describe('"sandbox" or "production" (default: production)'),
34
+ }),
35
+ annotations: { destructiveHint: false },
36
+ }, withErrorHandling(async (args) => {
37
+ const cid = args.clientId.trim();
38
+ const csecret = args.clientSecret.trim();
39
+ const rtoken = args.refreshToken.trim();
40
+ const rid = args.realmId.trim();
41
+ const env = args.environment?.trim().toLowerCase() ?? 'production';
42
+ if (!cid || !csecret || !rtoken || !rid) {
43
+ return JSON.stringify({ ok: false, error: 'clientId, clientSecret, refreshToken, and realmId are all required.' });
44
+ }
45
+ if (!validateEnvironment(env)) {
46
+ return JSON.stringify({ ok: false, error: 'environment must be "sandbox" or "production".' });
47
+ }
48
+ // Validate credentials by attempting a token refresh
49
+ const authHeader = 'Basic ' + Buffer.from(`${cid}:${csecret}`).toString('base64');
50
+ const body = new URLSearchParams({
51
+ grant_type: 'refresh_token',
52
+ refresh_token: rtoken,
53
+ });
54
+ let tokenResponse;
55
+ try {
56
+ tokenResponse = await fetch(TOKEN_URL, {
57
+ method: 'POST',
58
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
59
+ headers: {
60
+ Authorization: authHeader,
61
+ 'Content-Type': 'application/x-www-form-urlencoded',
62
+ Accept: 'application/json',
63
+ 'User-Agent': USER_AGENT,
64
+ },
65
+ body: body.toString(),
66
+ });
67
+ }
68
+ catch (error) {
69
+ if (error instanceof Error && error.name === 'TimeoutError') {
70
+ return JSON.stringify({
71
+ ok: false,
72
+ error: 'Token exchange request timed out.',
73
+ resolution: 'Verify your network connectivity.',
74
+ });
75
+ }
76
+ return JSON.stringify({
77
+ ok: false,
78
+ error: `Could not reach Intuit OAuth: ${error instanceof Error ? error.message : String(error)}`,
79
+ resolution: 'Verify your network connectivity.',
80
+ });
81
+ }
82
+ if (!tokenResponse.ok) {
83
+ const errorBody = await tokenResponse.json().catch(() => ({}));
84
+ return JSON.stringify({
85
+ ok: false,
86
+ error: `Invalid credentials: ${errorBody?.error_description || `HTTP ${tokenResponse.status}`}`,
87
+ resolution: 'Check your Client ID, Client Secret, and Refresh Token. Refresh tokens expire after 100 days of inactivity.',
88
+ });
89
+ }
90
+ // Persist via bridge
91
+ const result = await bridgeRequest('/bundled/quickbooks/configure', {
92
+ clientId: cid, clientSecret: csecret, refreshToken: rtoken, realmId: rid, environment: env,
93
+ });
94
+ if (!result.success) {
95
+ return JSON.stringify({ ok: false, error: result.error || 'Failed to configure QuickBooks via bridge.' });
96
+ }
97
+ // Update runtime credentials
98
+ setClientId(cid);
99
+ setClientSecret(csecret);
100
+ setRefreshToken(rtoken);
101
+ setRealmId(rid);
102
+ setEnvironment(env);
103
+ clearTokenCache();
104
+ const message = result.warning
105
+ ? `QuickBooks Online configured successfully. Note: ${result.warning}`
106
+ : 'QuickBooks Online configured successfully! Try list_quickbooks_invoices or list_quickbooks_customers.';
107
+ return JSON.stringify({ ok: true, message });
108
+ }));
109
+ }
110
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks customer tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerCustomerTools(server: McpServer): void;
6
+ //# sourceMappingURL=customers.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * QuickBooks customer tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboFetch, qboQuery } from '../client.js';
7
+ export function registerCustomerTools(server) {
8
+ server.registerTool('list_quickbooks_customers', {
9
+ description: `List customers from QuickBooks Online.
10
+
11
+ Returns: Id, DisplayName, PrimaryEmailAddr, PrimaryPhone, Balance, Active.
12
+
13
+ Example: {}
14
+ Example: { "active": true }
15
+ Example: { "searchTerm": "Smith" }`,
16
+ inputSchema: z.object({
17
+ active: z.boolean().optional().describe('Filter by active status'),
18
+ searchTerm: z.string().optional().describe('Search by display name (partial match)'),
19
+ limit: z.number().optional().describe('Max results (default: 50)'),
20
+ }),
21
+ annotations: { readOnlyHint: true },
22
+ }, withErrorHandling(async (args) => {
23
+ const limit = Math.min(args.limit ?? 50, 1000);
24
+ const conditions = [];
25
+ if (args.active !== undefined)
26
+ conditions.push(`Active = ${args.active}`);
27
+ if (args.searchTerm) {
28
+ conditions.push(`DisplayName LIKE '%${args.searchTerm.replace(/'/g, "\\'")}%'`);
29
+ }
30
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
31
+ const query = `SELECT * FROM Customer${where} ORDERBY DisplayName`;
32
+ const customers = await qboQuery('Customer', query, limit);
33
+ return JSON.stringify({ ok: true, customers, count: customers.length });
34
+ }));
35
+ server.registerTool('create_quickbooks_customer', {
36
+ description: `Create a new customer in QuickBooks Online.
37
+
38
+ Example: { "displayName": "Acme Corp" }
39
+ Example: { "displayName": "Jane Smith", "email": "jane@smith.com", "phone": "555-1234" }`,
40
+ inputSchema: z.object({
41
+ displayName: z.string().describe('Customer display name (required, must be unique)'),
42
+ email: z.string().optional().describe('Primary email address'),
43
+ phone: z.string().optional().describe('Primary phone number'),
44
+ companyName: z.string().optional().describe('Company name'),
45
+ }),
46
+ annotations: { readOnlyHint: false, destructiveHint: false },
47
+ }, withErrorHandling(async (args) => {
48
+ const customerBody = { DisplayName: args.displayName };
49
+ if (args.email)
50
+ customerBody.PrimaryEmailAddr = { Address: args.email };
51
+ if (args.phone)
52
+ customerBody.PrimaryPhone = { FreeFormNumber: args.phone };
53
+ if (args.companyName)
54
+ customerBody.CompanyName = args.companyName;
55
+ const result = await qboFetch('/customer?minorversion=65', { method: 'POST', body: JSON.stringify(customerBody) });
56
+ return JSON.stringify({ ok: true, message: 'Customer created.', customer: result.Customer });
57
+ }));
58
+ }
59
+ //# sourceMappingURL=customers.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks employee tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerEmployeeTools(server: McpServer): void;
6
+ //# sourceMappingURL=employees.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * QuickBooks employee tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboQuery } from '../client.js';
7
+ export function registerEmployeeTools(server) {
8
+ server.registerTool('list_quickbooks_employees', {
9
+ description: `List employees from QuickBooks Online.
10
+
11
+ Returns: Id, DisplayName, PrimaryEmailAddr, PrimaryPhone, Active.
12
+
13
+ Example: {}`,
14
+ inputSchema: z.object({
15
+ active: z.boolean().optional().describe('Filter by active status'),
16
+ limit: z.number().optional().describe('Max results (default: 50)'),
17
+ }),
18
+ annotations: { readOnlyHint: true },
19
+ }, withErrorHandling(async (args) => {
20
+ const limit = Math.min(args.limit ?? 50, 1000);
21
+ let where = '';
22
+ if (args.active !== undefined)
23
+ where = ` WHERE Active = ${args.active}`;
24
+ const query = `SELECT * FROM Employee${where} ORDERBY DisplayName`;
25
+ const employees = await qboQuery('Employee', query, limit);
26
+ return JSON.stringify({ ok: true, employees, count: employees.length });
27
+ }));
28
+ }
29
+ //# sourceMappingURL=employees.js.map
@@ -0,0 +1,9 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerQueryTools } from './query.js';
3
+ export { registerInvoiceTools } from './invoices.js';
4
+ export { registerCustomerTools } from './customers.js';
5
+ export { registerBillTools } from './bills.js';
6
+ export { registerVendorTools } from './vendors.js';
7
+ export { registerAccountTools } from './accounts.js';
8
+ export { registerEmployeeTools } from './employees.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ export { registerConfigureTools } from './configure.js';
2
+ export { registerQueryTools } from './query.js';
3
+ export { registerInvoiceTools } from './invoices.js';
4
+ export { registerCustomerTools } from './customers.js';
5
+ export { registerBillTools } from './bills.js';
6
+ export { registerVendorTools } from './vendors.js';
7
+ export { registerAccountTools } from './accounts.js';
8
+ export { registerEmployeeTools } from './employees.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks invoice tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerInvoiceTools(server: McpServer): void;
6
+ //# sourceMappingURL=invoices.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * QuickBooks invoice tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboFetch, qboQuery } from '../client.js';
7
+ export function registerInvoiceTools(server) {
8
+ server.registerTool('list_quickbooks_invoices', {
9
+ description: `List invoices from QuickBooks Online.
10
+
11
+ Returns: Id, DocNumber, TxnDate, DueDate, Balance, TotalAmt, CustomerRef, Line items.
12
+
13
+ Example: {}
14
+ Example: { "status": "unpaid" }
15
+ Example: { "customerId": "123" }
16
+
17
+ WORKFLOW:
18
+ 1. Call with no args to see recent invoices
19
+ 2. Filter by status (unpaid/paid/overdue) or customer
20
+ 3. Use get_quickbooks_entity for full invoice details`,
21
+ inputSchema: z.object({
22
+ status: z.enum(['unpaid', 'paid', 'overdue']).optional()
23
+ .describe('Filter: "unpaid", "paid", or "overdue"'),
24
+ customerId: z.string().optional().describe('Filter by customer ID'),
25
+ limit: z.number().optional().describe('Max results (default: 50)'),
26
+ }),
27
+ annotations: { readOnlyHint: true },
28
+ }, withErrorHandling(async (args) => {
29
+ const limit = Math.min(args.limit ?? 50, 1000);
30
+ const conditions = [];
31
+ if (args.status === 'unpaid')
32
+ conditions.push("Balance > '0'");
33
+ else if (args.status === 'paid')
34
+ conditions.push("Balance = '0'");
35
+ else if (args.status === 'overdue') {
36
+ conditions.push(`Balance > '0' AND DueDate < '${new Date().toISOString().split('T')[0]}'`);
37
+ }
38
+ if (args.customerId)
39
+ conditions.push(`CustomerRef = '${args.customerId}'`);
40
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
41
+ const query = `SELECT * FROM Invoice${where} ORDERBY TxnDate DESC`;
42
+ const invoices = await qboQuery('Invoice', query, limit);
43
+ return JSON.stringify({ ok: true, invoices, count: invoices.length });
44
+ }));
45
+ server.registerTool('create_quickbooks_invoice', {
46
+ description: `Create a new invoice in QuickBooks Online.
47
+
48
+ Example: { "customerId": "123", "lines": [{ "description": "Consulting services", "amount": 1500 }] }
49
+
50
+ WORKFLOW:
51
+ 1. Use list_quickbooks_customers to find the customer ID
52
+ 2. Create with line items (description + amount required)
53
+ 3. Optionally set dueDate and memo
54
+
55
+ COMMON MISTAKES:
56
+ - customerId is required (use list_quickbooks_customers to find it)
57
+ - Each line needs at least description and amount
58
+ - Dates use YYYY-MM-DD format`,
59
+ inputSchema: z.object({
60
+ customerId: z.string().describe('Customer ID (required)'),
61
+ lines: z.array(z.object({
62
+ description: z.string().describe('Line description'),
63
+ amount: z.number().describe('Line amount'),
64
+ qty: z.number().optional().describe('Quantity (default: 1)'),
65
+ itemId: z.string().optional().describe('Item/service ID (optional)'),
66
+ })).describe('Invoice line items'),
67
+ dueDate: z.string().optional().describe('Due date (YYYY-MM-DD)'),
68
+ memo: z.string().optional().describe('Customer memo / notes'),
69
+ }),
70
+ annotations: { readOnlyHint: false, destructiveHint: false },
71
+ }, withErrorHandling(async (args) => {
72
+ const invoiceBody = {
73
+ CustomerRef: { value: args.customerId },
74
+ Line: args.lines.map((line) => ({
75
+ Amount: line.amount,
76
+ DetailType: 'SalesItemLineDetail',
77
+ Description: line.description,
78
+ SalesItemLineDetail: {
79
+ Qty: line.qty || 1,
80
+ UnitPrice: line.amount / (line.qty || 1),
81
+ ...(line.itemId ? { ItemRef: { value: line.itemId } } : {}),
82
+ },
83
+ })),
84
+ };
85
+ if (args.dueDate)
86
+ invoiceBody.DueDate = args.dueDate;
87
+ if (args.memo)
88
+ invoiceBody.CustomerMemo = { value: args.memo };
89
+ const result = await qboFetch('/invoice?minorversion=65', { method: 'POST', body: JSON.stringify(invoiceBody) });
90
+ return JSON.stringify({ ok: true, message: 'Invoice created.', invoice: result.Invoice });
91
+ }));
92
+ }
93
+ //# sourceMappingURL=invoices.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks query and entity retrieval tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerQueryTools(server: McpServer): void;
6
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * QuickBooks query and entity retrieval tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboFetch, qboQuery } from '../client.js';
7
+ export function registerQueryTools(server) {
8
+ server.registerTool('query_quickbooks', {
9
+ description: `Run a QuickBooks query using the QuickBooks Query Language.
10
+
11
+ Returns matching entities. QuickBooks uses a SQL-like query language.
12
+
13
+ Example: { "query": "SELECT * FROM Invoice WHERE Balance > '0' ORDERBY DueDate" }
14
+ Example: { "query": "SELECT * FROM Customer WHERE Active = true" }
15
+
16
+ WORKFLOW:
17
+ 1. Use this for flexible searches across any entity type
18
+ 2. Entity names are PascalCase: Invoice, Customer, Vendor, Bill, Employee, etc.
19
+ 3. String values use single quotes, dates use 'YYYY-MM-DD' format
20
+
21
+ COMMON MISTAKES:
22
+ - Entity names are case-sensitive PascalCase (Invoice, not invoice)
23
+ - Use single quotes for string/date values, not double quotes
24
+ - LIKE operator uses % wildcard: DisplayName LIKE '%Smith%'`,
25
+ inputSchema: z.object({
26
+ query: z.string().describe('QuickBooks Query Language statement'),
27
+ limit: z.number().optional().describe('Max results (default: 100, max: 1000)'),
28
+ }),
29
+ annotations: { readOnlyHint: true },
30
+ }, withErrorHandling(async (args) => {
31
+ const query = args.query;
32
+ const limit = Math.min(args.limit ?? 100, 1000);
33
+ // Extract entity name from query for response parsing
34
+ const entityMatch = query.match(/FROM\s+(\w+)/i);
35
+ const entityName = entityMatch ? entityMatch[1] : 'Unknown';
36
+ const results = await qboQuery(entityName, query, limit);
37
+ return JSON.stringify({ ok: true, entity: entityName, data: results, count: results.length });
38
+ }));
39
+ server.registerTool('get_quickbooks_entity', {
40
+ description: `Get a single QuickBooks entity by type and ID.
41
+
42
+ Example: { "entityType": "Invoice", "entityId": "123" }
43
+
44
+ Supported entity types: Account, Bill, BillPayment, Customer, Employee, Estimate, Invoice, Item, JournalEntry, Purchase, Vendor`,
45
+ inputSchema: z.object({
46
+ entityType: z.enum([
47
+ 'Account', 'Bill', 'BillPayment', 'Customer', 'Employee',
48
+ 'Estimate', 'Invoice', 'Item', 'JournalEntry', 'Purchase', 'Vendor',
49
+ ]).describe('Entity type (PascalCase)'),
50
+ entityId: z.string().describe('Entity ID'),
51
+ }),
52
+ annotations: { readOnlyHint: true },
53
+ }, withErrorHandling(async (args) => {
54
+ const { entityType, entityId } = args;
55
+ const result = await qboFetch(`/${entityType.toLowerCase()}/${encodeURIComponent(entityId)}?minorversion=65`);
56
+ return JSON.stringify({ ok: true, [entityType]: result[entityType] || result });
57
+ }));
58
+ }
59
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * QuickBooks vendor tools.
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ export declare function registerVendorTools(server: McpServer): void;
6
+ //# sourceMappingURL=vendors.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * QuickBooks vendor tools.
3
+ */
4
+ import { z } from 'zod';
5
+ import { withErrorHandling } from '../utils.js';
6
+ import { qboFetch, qboQuery } from '../client.js';
7
+ export function registerVendorTools(server) {
8
+ server.registerTool('list_quickbooks_vendors', {
9
+ description: `List vendors from QuickBooks Online.
10
+
11
+ Returns: Id, DisplayName, PrimaryEmailAddr, PrimaryPhone, Balance, Active.
12
+
13
+ Example: {}
14
+ Example: { "searchTerm": "Office" }`,
15
+ inputSchema: z.object({
16
+ active: z.boolean().optional().describe('Filter by active status'),
17
+ searchTerm: z.string().optional().describe('Search by display name (partial match)'),
18
+ limit: z.number().optional().describe('Max results (default: 50)'),
19
+ }),
20
+ annotations: { readOnlyHint: true },
21
+ }, withErrorHandling(async (args) => {
22
+ const limit = Math.min(args.limit ?? 50, 1000);
23
+ const conditions = [];
24
+ if (args.active !== undefined)
25
+ conditions.push(`Active = ${args.active}`);
26
+ if (args.searchTerm) {
27
+ conditions.push(`DisplayName LIKE '%${args.searchTerm.replace(/'/g, "\\'")}%'`);
28
+ }
29
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
30
+ const query = `SELECT * FROM Vendor${where} ORDERBY DisplayName`;
31
+ const vendors = await qboQuery('Vendor', query, limit);
32
+ return JSON.stringify({ ok: true, vendors, count: vendors.length });
33
+ }));
34
+ server.registerTool('create_quickbooks_vendor', {
35
+ description: `Create a new vendor in QuickBooks Online.
36
+
37
+ Example: { "displayName": "Office Depot" }
38
+ Example: { "displayName": "AWS", "email": "billing@aws.amazon.com", "companyName": "Amazon Web Services" }`,
39
+ inputSchema: z.object({
40
+ displayName: z.string().describe('Vendor display name (required, must be unique)'),
41
+ email: z.string().optional().describe('Primary email address'),
42
+ phone: z.string().optional().describe('Primary phone number'),
43
+ companyName: z.string().optional().describe('Company name'),
44
+ }),
45
+ annotations: { readOnlyHint: false, destructiveHint: false },
46
+ }, withErrorHandling(async (args) => {
47
+ const vendorBody = { DisplayName: args.displayName };
48
+ if (args.email)
49
+ vendorBody.PrimaryEmailAddr = { Address: args.email };
50
+ if (args.phone)
51
+ vendorBody.PrimaryPhone = { FreeFormNumber: args.phone };
52
+ if (args.companyName)
53
+ vendorBody.CompanyName = args.companyName;
54
+ const result = await qboFetch('/vendor?minorversion=65', { method: 'POST', body: JSON.stringify(vendorBody) });
55
+ return JSON.stringify({ ok: true, message: 'Vendor created.', vendor: result.Vendor });
56
+ }));
57
+ }
58
+ //# sourceMappingURL=vendors.js.map
@@ -0,0 +1,13 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 30000;
2
+ export declare const USER_AGENT = "MindstoneRebel/1.0 (QuickBooks-MCP)";
3
+ export declare const TOKEN_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";
4
+ export interface BridgeState {
5
+ port: number;
6
+ token: string;
7
+ }
8
+ export declare class QuickBooksError extends Error {
9
+ readonly code: string;
10
+ readonly resolution: string;
11
+ constructor(message: string, code: string, resolution: string);
12
+ }
13
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ export const REQUEST_TIMEOUT_MS = 30_000;
2
+ export const USER_AGENT = 'MindstoneRebel/1.0 (QuickBooks-MCP)';
3
+ export const TOKEN_URL = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer';
4
+ export class QuickBooksError extends Error {
5
+ code;
6
+ resolution;
7
+ constructor(message, code, resolution) {
8
+ super(message);
9
+ this.code = code;
10
+ this.resolution = resolution;
11
+ this.name = 'QuickBooksError';
12
+ }
13
+ }
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,14 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
3
+ /**
4
+ * Wraps a tool handler with standard error handling.
5
+ *
6
+ * - On success: returns the string result as a text content block.
7
+ * - On QuickBooksError: returns a structured JSON error with code and resolution.
8
+ * - On unknown error: returns a generic error message.
9
+ *
10
+ * Secrets are never exposed in error messages.
11
+ */
12
+ export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
13
+ export {};
14
+ //# sourceMappingURL=utils.d.ts.map
package/dist/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ import { QuickBooksError } from './types.js';
2
+ /**
3
+ * Wraps a tool handler with standard error handling.
4
+ *
5
+ * - On success: returns the string result as a text content block.
6
+ * - On QuickBooksError: returns a structured JSON error with code and resolution.
7
+ * - On unknown error: returns a generic error message.
8
+ *
9
+ * Secrets are never exposed in error messages.
10
+ */
11
+ export function withErrorHandling(fn) {
12
+ return async (args, extra) => {
13
+ try {
14
+ const result = await fn(args, extra);
15
+ return { content: [{ type: 'text', text: result }] };
16
+ }
17
+ catch (error) {
18
+ if (error instanceof QuickBooksError) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify({
24
+ ok: false,
25
+ error: error.message,
26
+ code: error.code,
27
+ resolution: error.resolution,
28
+ }),
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+ const errorMessage = error instanceof Error ? error.message : String(error);
35
+ return {
36
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
37
+ isError: true,
38
+ };
39
+ }
40
+ };
41
+ }
42
+ //# sourceMappingURL=utils.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mindstone-engineering/mcp-server-quickbooks",
3
+ "version": "0.1.0",
4
+ "description": "QuickBooks Online MCP server for Model Context Protocol hosts — invoices, bills, customers, vendors, accounts",
5
+ "license": "FSL-1.1-MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-quickbooks": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "!dist/**/*.map"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/nspr-io/mcp-servers.git",
17
+ "directory": "connectors/quickbooks"
18
+ },
19
+ "homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/quickbooks",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc && shx chmod +x dist/index.js",
25
+ "prepare": "npm run build",
26
+ "watch": "tsc --watch",
27
+ "start": "node dist/index.js",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage"
31
+ },
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "devDependencies": {
37
+ "@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
38
+ "@types/node": "^22",
39
+ "@vitest/coverage-v8": "^4.1.3",
40
+ "msw": "^2.13.2",
41
+ "shx": "^0.3.4",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^4.1.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }