xero-mcp 1.3.0 → 2.0.0-beta

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 John Zhang
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 John Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,109 +1,129 @@
1
- # Xero MCP Server
2
-
3
- ![](https://badge.mcpx.dev?type=server "MCP Server")
4
- [![smithery badge](https://smithery.ai/badge/xero-mcp)](https://smithery.ai/server/@john-zhang-dev/xero-mcp)
5
-
6
- This MCP server allows Clients to interact with [Xero Accounting Software](https://www.xero.com).
7
-
8
- ## Get Started
9
-
10
- 1. Make sure [node](https://nodejs.org) and [Claude Desktop](https://claude.ai/download) are installed.
11
-
12
- 2. Create an OAuth 2.0 app in Xero to get a _CLIENT_ID_ and _CLIENT_SECRET_.
13
-
14
- - Create a free Xero user account (if you don't have one)
15
- - Login to Xero Developer center https://developer.xero.com/app/manage/
16
- - Click New app
17
- - Enter a name for your app
18
- - Select Web app
19
- - Provide a valid URL (can be anything valid eg. https://www.myapp.com)
20
- - Enter redirect URI: `http://localhost:5000/callback`
21
- - Tick to Accept the Terms & Conditions and click Create app
22
- - On the left-hand side of the screen select Configuration
23
- - Click Generate a secret
24
-
25
- 3. Modify `claude_desktop_config.json` file
26
-
27
- ```json
28
- {
29
- "mcpServers": {
30
- "xero-mcp": {
31
- "command": "npx",
32
- "args": ["-y", "xero-mcp@latest"],
33
- "env": {
34
- "XERO_CLIENT_ID": "YOUR_CLIENT_ID",
35
- "XERO_CLIENT_SECRET": "YOUR_CLIENT_SECRET",
36
- "XERO_REDIRECT_URI": "http://localhost:5000/callback"
37
- }
38
- }
39
- }
40
- }
41
- ```
42
-
43
- 4. Restart Claude Desktop
44
-
45
- 5. When the Client decides to access a Xero tool for the first time, a Xero login page will pop up to ask your consent. Complete the auth flow and manually close the web page (as the Xero page will not auto close in this version)
46
-
47
- **Privacy alert: after completing the Xero OAuth2 flow, your Xero data may go through the LLM that you use. If you are doing testing you should authorize to your [Xero Demo Company](https://central.xero.com/s/article/Use-the-demo-company).**
48
-
49
- ## Tools
50
-
51
- - `authenticate`
52
-
53
- Authenticate with Xero using OAuth2
54
-
55
- - `get_balance_sheet`
56
-
57
- Retrieves report for balancesheet
58
-
59
- - `list_accounts`
60
-
61
- Retrieves the full chart of accounts
62
-
63
- - `list_bank_transactions`
64
-
65
- Retrieves any spent or received money transactions
66
-
67
- - `list_contacts`
68
-
69
- Retrieves all contacts in a Xero organisation
70
-
71
- - `list_invoices`
72
-
73
- Retrieves sales invoices or purchase bills
74
-
75
- - `list_journals`
76
-
77
- Retrieves journals
78
-
79
- - `list_organisations`
80
-
81
- Retrieves Xero organisation details
82
-
83
- - `list_payments`
84
-
85
- Retrieves payments for invoices and credit notes
86
-
87
- - `list_quotes`
88
-
89
- Retrieves sales quotes
90
-
91
- ## Examples
92
-
93
- - "Visualize my financial position over the last month"
94
-
95
- <img src="https://github.com/john-zhang-dev/assets/blob/main/xero-mcp/demo1.jpg?raw=true" width=50% height=50%>
96
-
97
- - "Track my spendings over last week"
98
-
99
- <img src="https://github.com/john-zhang-dev/assets/blob/main/xero-mcp/demo2.jpg?raw=true" width=50% height=50%>
100
-
101
- ## WIP Features
102
-
103
- - Tools that allow new transactions to be added to Xero
104
-
105
- If you have additional requirements, please open an issue on this github repository.
106
-
107
- ## License
108
-
109
- MIT
1
+ # Xero MCP Server
2
+
3
+ ![](https://badge.mcpx.dev?type=server "MCP Server")
4
+ [![smithery badge](https://smithery.ai/badge/@john-zhang-dev/xero-mcp)](https://smithery.ai/server/@john-zhang-dev/xero-mcp)
5
+
6
+ This MCP server allows Clients to interact with [Xero Accounting Software](https://www.xero.com).
7
+
8
+ ## Get Started
9
+
10
+ 1. Make sure [node](https://nodejs.org) and [Claude Desktop](https://claude.ai/download) are installed.
11
+
12
+ 2. Create an OAuth 2.0 app in Xero to get a _CLIENT_ID_ and _CLIENT_SECRET_.
13
+
14
+ - Create a free Xero user account (if you don't have one)
15
+ - Login to Xero Developer center https://developer.xero.com/app/manage/
16
+ - Click New app
17
+ - Enter a name for your app
18
+ - Select Web app
19
+ - Provide a valid URL (can be anything valid eg. https://www.myapp.com)
20
+ - Enter redirect URI: `http://localhost:5000/callback`
21
+ - Tick to Accept the Terms & Conditions and click Create app
22
+ - On the left-hand side of the screen select Configuration
23
+ - Click Generate a secret
24
+
25
+ 3. Modify `claude_desktop_config.json` file:
26
+
27
+ Go to Settings -> Developers -> Local MCP Servers -> Edit Config
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "xero-mcp": {
33
+ "command": "npx",
34
+ "args": ["-y", "xero-mcp@latest"],
35
+ "env": {
36
+ "XERO_CLIENT_ID": "YOUR_CLIENT_ID",
37
+ "XERO_CLIENT_SECRET": "YOUR_CLIENT_SECRET",
38
+ "XERO_REDIRECT_URI": "http://localhost:5000/callback"
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ 4. Restart Claude Desktop, or from Claude Desktop Menu:
46
+
47
+ Developer -> Reload MCP Configuration
48
+
49
+ 5. When the Client decides to access a Xero tool for the first time, a Xero login page will pop up to ask your consent. Complete the auth flow and manually close the web page (as the Xero page will not auto close in this version)
50
+
51
+ **Privacy alert: after completing the Xero OAuth2 flow, your Xero data may go through the LLM that you use. If you are doing testing you should authorize to your [Xero Demo Company](https://central.xero.com/s/article/Use-the-demo-company).**
52
+
53
+ ## Tools
54
+
55
+ - `authenticate`
56
+
57
+ Authenticate with Xero using OAuth2
58
+
59
+ - `create_bank_transactions`
60
+
61
+ Creates one or more spent or received money transaction
62
+
63
+ - `create_contacts`
64
+
65
+ Creates one or multiple contacts in a Xero organisation
66
+
67
+ - `get_balance_sheet`
68
+
69
+ Retrieves report for balancesheet
70
+
71
+ - `get_bank_transaction`
72
+
73
+ Retrieves a single spent or received money transaction by its Xero bank transaction ID
74
+
75
+ - `get_invoice`
76
+
77
+ Retrieves a single sales invoice or purchase bill by its Xero invoice ID
78
+
79
+ - `list_accounts`
80
+
81
+ Retrieves the full chart of accounts
82
+
83
+ - `list_bank_transactions`
84
+
85
+ Retrieves any spent or received money transactions
86
+
87
+ - `list_contacts`
88
+
89
+ Retrieves all contacts in a Xero organisation
90
+
91
+ - `list_invoices`
92
+
93
+ Retrieves sales invoices or purchase bills
94
+
95
+ - `list_organisations`
96
+
97
+ Retrieves Xero organisation details
98
+
99
+ - `list_payments`
100
+
101
+ Retrieves payments for invoices and credit notes
102
+
103
+ - `list_quotes`
104
+
105
+ Retrieves sales quotes
106
+
107
+ - `update_bank_transaction`
108
+
109
+ Updates an existing spent or received money transaction (e.g. line items, contact, bank account) by bank transaction ID
110
+
111
+ - `update_invoice`
112
+
113
+ Updates an existing sales invoice or purchase bill (typically a draft), including line items and account codes
114
+
115
+ ## Examples
116
+
117
+ - "Visualize my financial position over the last month"
118
+
119
+ <img src="https://github.com/john-zhang-dev/assets/blob/main/xero-mcp/demo1.jpg?raw=true" width=50% height=50%>
120
+
121
+ - "Track my spendings over last week"
122
+
123
+ <img src="https://github.com/john-zhang-dev/assets/blob/main/xero-mcp/demo2.jpg?raw=true" width=50% height=50%>
124
+
125
+ - "Add all transactions from the monthly statement into my revenue account (account code 201) as receive money"
126
+
127
+ ## License
128
+
129
+ MIT
@@ -1,9 +1,44 @@
1
1
  import { z } from "zod";
2
2
  import { XeroClientSession } from "../../XeroApiClient.js";
3
3
  import { XeroAccountingApiSchema } from "../../Resources/xero_accounting.js";
4
- import { parseArrayValues } from "../Utils/parseArrayValues.js";
5
- import { convertToCamelCase } from "../Utils/convertToCamelCase.js";
6
- import { sanitizeObject } from "../Utils/sanitizeValues.js";
4
+ import { parseArrayValues } from "../../Utils/parseArrayValues.js";
5
+ import { convertToCamelCase } from "../../Utils/convertToCamelCase.js";
6
+ import { sanitizeObject } from "../../Utils/sanitizeValues.js";
7
+ export const GetBankTransactionTool = {
8
+ requestSchema: {
9
+ name: "get_bank_transaction",
10
+ description: "Retrieves a single spent or received money transaction by its Xero bank transaction ID",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ bankTransactionID: {
15
+ type: "string",
16
+ description: "Xero-generated unique identifier for the bank transaction (UUID)",
17
+ },
18
+ unitdp: {
19
+ type: "number",
20
+ description: "Optional. Unit decimal places (e.g. 4) for unit amounts on line items",
21
+ },
22
+ },
23
+ required: ["bankTransactionID"],
24
+ },
25
+ output: { content: [{ type: "text", text: z.string() }] },
26
+ },
27
+ requestHandler: async (request) => {
28
+ const bankTransactionID = request.params.arguments
29
+ ?.bankTransactionID;
30
+ const unitdp = request.params.arguments?.unitdp;
31
+ const response = await XeroClientSession.xeroClient.accountingApi.getBankTransaction(XeroClientSession.activeTenantId(), bankTransactionID, unitdp);
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text",
36
+ text: JSON.stringify(response.body.bankTransactions ?? []),
37
+ },
38
+ ],
39
+ };
40
+ },
41
+ };
7
42
  export const ListBankTransactionsTool = {
8
43
  requestSchema: {
9
44
  name: "list_bank_transactions",
@@ -56,3 +91,56 @@ export const CreateBankTransactionsTool = {
56
91
  return { content: [{ type: "text", text: JSON.stringify(response.body) }] };
57
92
  },
58
93
  };
94
+ export const UpdateBankTransactionTool = {
95
+ requestSchema: {
96
+ name: "update_bank_transaction",
97
+ description: "Updates an existing spent or received money transaction by its Xero bank transaction ID",
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ bankTransactionID: {
102
+ type: "string",
103
+ description: "Xero generated unique identifier for the bank transaction (UUID)",
104
+ },
105
+ bankTransactions: {
106
+ type: "object",
107
+ description: "BankTransactions payload containing an array of bank transaction objects",
108
+ properties: XeroAccountingApiSchema.components.schemas.BankTransactions.properties,
109
+ example: '{ bankTransactions: [{ type: "SPEND", date: "2026-01-01", reference: "Expense Update", subTotal: 100, total: 115, totalTax: 15, lineItems: [{ accountCode: "401", description: "Taxi fare", lineAmount: 115 }], contact: { contactID: "00000000-0000-0000-0000-000000000000" }, bankAccount: { accountID: "6f7594f2-f059-4d56-9e67-47ac9733bfe9" }, status: "AUTHORISED" }]}',
110
+ },
111
+ unitdp: {
112
+ type: "number",
113
+ description: "Optional. Unit decimal places (e.g. 4) for unit amounts on line items",
114
+ },
115
+ idempotencyKey: {
116
+ type: "string",
117
+ description: "Optional idempotency key. Allows safe retries without duplicating processing",
118
+ },
119
+ },
120
+ required: ["bankTransactionID", "bankTransactions"],
121
+ },
122
+ output: { content: [{ type: "text", text: z.string() }] },
123
+ },
124
+ requestHandler: async (request) => {
125
+ const rawInputData = request.params.arguments;
126
+ const parsedData = parseArrayValues(rawInputData);
127
+ const bankTransactionID = parsedData?.bankTransactionID;
128
+ const unitdp = parsedData?.unitdp;
129
+ const idempotencyKey = parsedData?.idempotencyKey;
130
+ const rawBankTransactionsPayload = parsedData?.bankTransactions;
131
+ const bankTransactionsPayload = sanitizeObject(convertToCamelCase(rawBankTransactionsPayload));
132
+ if (!bankTransactionID) {
133
+ // Should be prevented by request schema, but keep a hard guard.
134
+ throw new Error("Missing required parameter: bankTransactionID");
135
+ }
136
+ const response = await XeroClientSession.xeroClient.accountingApi.updateBankTransaction(XeroClientSession.activeTenantId(), bankTransactionID, bankTransactionsPayload, unitdp, idempotencyKey);
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify(response.body ?? response.response?.status),
142
+ },
143
+ ],
144
+ };
145
+ },
146
+ };
@@ -1,9 +1,9 @@
1
1
  import { XeroClientSession } from "../../XeroApiClient.js";
2
2
  import { z } from "zod";
3
3
  import { XeroAccountingApiSchema } from "../../Resources/xero_accounting.js";
4
- import { parseArrayValues } from "../Utils/parseArrayValues.js";
5
- import { convertToCamelCase } from "../Utils/convertToCamelCase.js";
6
- import { sanitizeObject } from "../Utils/sanitizeValues.js";
4
+ import { parseArrayValues } from "../../Utils/parseArrayValues.js";
5
+ import { convertToCamelCase } from "../../Utils/convertToCamelCase.js";
6
+ import { sanitizeObject } from "../../Utils/sanitizeValues.js";
7
7
  export const ListContactsTool = {
8
8
  requestSchema: {
9
9
  name: "list_contacts",
@@ -1,5 +1,9 @@
1
1
  import { XeroClientSession } from "../../XeroApiClient.js";
2
2
  import { z } from "zod";
3
+ import { XeroAccountingApiSchema } from "../../Resources/xero_accounting.js";
4
+ import { parseArrayValues } from "../../Utils/parseArrayValues.js";
5
+ import { convertToCamelCase } from "../../Utils/convertToCamelCase.js";
6
+ import { sanitizeObject } from "../../Utils/sanitizeValues.js";
3
7
  export const ListInvoicesTool = {
4
8
  requestSchema: {
5
9
  name: "list_invoices",
@@ -32,3 +36,90 @@ export const ListInvoicesTool = {
32
36
  };
33
37
  },
34
38
  };
39
+ export const GetInvoiceTool = {
40
+ requestSchema: {
41
+ name: "get_invoice",
42
+ description: "Retrieves a single sales invoice or purchase bill by its Xero invoice ID",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ invoiceID: {
47
+ type: "string",
48
+ description: "Xero-generated unique identifier for the invoice (UUID)",
49
+ },
50
+ unitdp: {
51
+ type: "number",
52
+ description: "Optional. Unit decimal places (e.g. 4) for unit amounts on line items",
53
+ },
54
+ },
55
+ required: ["invoiceID"],
56
+ },
57
+ output: { content: [{ type: "text", text: z.string() }] },
58
+ },
59
+ requestHandler: async (request) => {
60
+ const invoiceID = request.params.arguments?.invoiceID;
61
+ const unitdp = request.params.arguments?.unitdp;
62
+ const response = await XeroClientSession.xeroClient.accountingApi.getInvoice(XeroClientSession.activeTenantId(), invoiceID, unitdp);
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify(response.body.invoices ?? []),
68
+ },
69
+ ],
70
+ };
71
+ },
72
+ };
73
+ export const UpdateInvoiceTool = {
74
+ requestSchema: {
75
+ name: "update_invoice",
76
+ description: "Updates an existing sales invoice or purchase bill (typically a draft) to change fields like line items and account codes",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ invoiceID: {
81
+ type: "string",
82
+ description: "Xero generated unique identifier for the invoice (UUID)",
83
+ },
84
+ invoices: {
85
+ type: "object",
86
+ description: "Invoices payload containing an array of invoice objects",
87
+ properties: XeroAccountingApiSchema.components.schemas.Invoices.properties,
88
+ example: '{ invoices: [{ type: "ACCREC", contact: { contactId: "00000000-0000-0000-0000-000000000000" }, date: "2026-01-01", dueDate: "2026-01-15", lineItems: [{ description: "Service", quantity: 1, unitAmount: 100, accountCode: "400", tracking: [] }], reference: "Website Design", status: "DRAFT" }]}',
89
+ },
90
+ unitdp: {
91
+ type: "number",
92
+ description: "Optional. Unit decimal places (e.g. 4) for unit amounts on line items",
93
+ },
94
+ idempotencyKey: {
95
+ type: "string",
96
+ description: "Optional idempotency key. Allows safe retries without duplicating processing",
97
+ },
98
+ },
99
+ required: ["invoiceID", "invoices"],
100
+ },
101
+ output: { content: [{ type: "text", text: z.string() }] },
102
+ },
103
+ requestHandler: async (request) => {
104
+ const rawInputData = request.params.arguments;
105
+ const parsedData = parseArrayValues(rawInputData);
106
+ const invoiceID = parsedData?.invoiceID;
107
+ const unitdp = parsedData?.unitdp;
108
+ const idempotencyKey = parsedData?.idempotencyKey;
109
+ const rawInvoicesPayload = parsedData?.invoices;
110
+ const invoicesPayload = sanitizeObject(convertToCamelCase(rawInvoicesPayload));
111
+ if (!invoiceID) {
112
+ // Should be prevented by request schema, but keep a hard guard.
113
+ throw new Error("Missing required parameter: invoiceID");
114
+ }
115
+ const response = await XeroClientSession.xeroClient.accountingApi.updateInvoice(XeroClientSession.activeTenantId(), invoiceID, invoicesPayload, unitdp, idempotencyKey);
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: JSON.stringify(response.body ?? response.response?.status),
121
+ },
122
+ ],
123
+ };
124
+ },
125
+ };
@@ -1,9 +1,8 @@
1
1
  import { ListAccountsTool } from "./Accounting/Accounts.js";
2
2
  import { AuthenticateTool } from "./Authenticate.js";
3
- import { CreateBankTransactionsTool, ListBankTransactionsTool } from "./Accounting/BankTransactions.js";
3
+ import { CreateBankTransactionsTool, GetBankTransactionTool, ListBankTransactionsTool, UpdateBankTransactionTool, } from "./Accounting/BankTransactions.js";
4
4
  import { CreateContactsTool, ListContactsTool } from "./Accounting/Contacts.js";
5
- import { ListInvoicesTool } from "./Accounting/Invoices.js";
6
- import { ListJournalsTool } from "./Accounting/Journals.js";
5
+ import { GetInvoiceTool, ListInvoicesTool, UpdateInvoiceTool, } from "./Accounting/Invoices.js";
7
6
  import { ListOrganisationsTool } from "./Accounting/Organisations.js";
8
7
  import { ListPaymentsTool } from "./Accounting/Payments.js";
9
8
  import { ListQuotesTool } from "./Accounting/Quotes.js";
@@ -14,14 +13,17 @@ export const McpToolsFactory = (function () {
14
13
  CreateBankTransactionsTool,
15
14
  CreateContactsTool,
16
15
  GetBalanceSheetTool,
16
+ GetBankTransactionTool,
17
+ GetInvoiceTool,
17
18
  ListAccountsTool,
18
19
  ListBankTransactionsTool,
19
20
  ListContactsTool,
20
21
  ListInvoicesTool,
21
- ListJournalsTool,
22
22
  ListOrganisationsTool,
23
23
  ListPaymentsTool,
24
24
  ListQuotesTool,
25
+ UpdateBankTransactionTool,
26
+ UpdateInvoiceTool,
25
27
  // register new tools here alphabetically
26
28
  ];
27
29
  return {
@@ -3,25 +3,33 @@
3
3
  * 1. Removing javascript protocol
4
4
  * 2. Removing HTML tags
5
5
  * 3. Escaping special characters
6
- * 4. Trimming whitespace
6
+ * 4. Preventing SQL injections
7
+ * 5. Trimming whitespace
7
8
  */
8
9
  export function sanitizeValue(value) {
9
- return value
10
- // Remove javascript protocol
11
- .replace(/javascript:/gi, '')
12
- // Remove HTML tags
13
- .replace(/<[^>]*>/g, '')
14
- // Escape special characters (excluding quotes)
15
- .replace(/[&<>]/g, char => {
10
+ if (typeof value !== 'string') {
11
+ return value;
12
+ }
13
+ let result = value;
14
+ // Remove javascript protocol
15
+ result = result.replace(/javascript:/gi, '');
16
+ // Remove HTML tags
17
+ result = result.replace(/<[^>]*>/g, '');
18
+ // Escape HTML special characters
19
+ result = result.replace(/[&<>]/g, char => {
16
20
  const escapeMap = {
17
21
  '&': '&amp;',
18
22
  '<': '&lt;',
19
23
  '>': '&gt;'
20
24
  };
21
25
  return escapeMap[char];
22
- })
23
- // Trim whitespace
24
- .trim();
26
+ });
27
+ // Prevent SQL injection - escape single quotes
28
+ result = result.replace(/(['"])/g, match => {
29
+ return match === "'" ? "''" : match;
30
+ });
31
+ // Trim whitespace
32
+ return result.trim();
25
33
  }
26
34
  /**
27
35
  * Sanitizes all string values in an object recursively
@@ -14,6 +14,19 @@ describe('sanitizeValue', () => {
14
14
  expect(sanitizeValue('<')).toBe('&lt;');
15
15
  expect(sanitizeValue('>')).toBe('&gt;');
16
16
  });
17
+ it('prevents SQL injection', () => {
18
+ // Basic SQL injection patterns
19
+ expect(sanitizeValue("O'Connor")).toBe("O''Connor");
20
+ expect(sanitizeValue("' OR '1'='1")).toBe("'' OR ''1''=''1");
21
+ expect(sanitizeValue("'; DROP TABLE users; --")).toBe("''; DROP TABLE users; --");
22
+ // More complex SQL injection patterns
23
+ expect(sanitizeValue("' UNION SELECT username, password FROM users --")).toBe("'' UNION SELECT username, password FROM users --");
24
+ expect(sanitizeValue("' OR 1=1; UPDATE users SET password='hacked' WHERE username='admin'; --")).toBe("'' OR 1=1; UPDATE users SET password=''hacked'' WHERE username=''admin''; --");
25
+ expect(sanitizeValue("admin' --")).toBe("admin'' --");
26
+ expect(sanitizeValue("1' OR '1' = '1")).toBe("1'' OR ''1'' = ''1");
27
+ // Double quotes should remain unchanged (handled by parameterized queries)
28
+ expect(sanitizeValue('User "Admin"')).toBe('User "Admin"');
29
+ });
17
30
  it('trims whitespace', () => {
18
31
  expect(sanitizeValue(' hello ')).toBe('hello');
19
32
  expect(sanitizeValue('\n\t\r hello \n\t\r')).toBe('hello');
@@ -21,11 +34,14 @@ describe('sanitizeValue', () => {
21
34
  it('handles multiple sanitization requirements', () => {
22
35
  expect(sanitizeValue(' javascript:alert("<script>") ')).toBe('alert("")');
23
36
  expect(sanitizeValue('<div>Hello & World</div>')).toBe('Hello &amp; World');
37
+ expect(sanitizeValue(" <script>alert('DROP TABLE users');</script> ")).toBe("alert(''DROP TABLE users'');");
38
+ expect(sanitizeValue(" <img src='x' onerror='alert(\"1\")'>admin' OR '1'='1 ")).toBe("admin'' OR ''1''=''1");
24
39
  });
25
40
  });
26
41
  describe('sanitizeObject', () => {
27
42
  it('handles primitive values', () => {
28
43
  expect(sanitizeObject(' <script>alert("xss")</script> ')).toBe('alert("xss")');
44
+ expect(sanitizeObject("O'Connor")).toBe("O''Connor");
29
45
  expect(sanitizeObject(123)).toBe(123);
30
46
  expect(sanitizeObject(true)).toBe(true);
31
47
  expect(sanitizeObject(null)).toBe(null);
@@ -35,12 +51,16 @@ describe('sanitizeObject', () => {
35
51
  const input = [
36
52
  ' <script>alert("xss")</script> ',
37
53
  'javascript:alert("xss")',
38
- 'Hello & World'
54
+ 'Hello & World',
55
+ "admin' OR '1'='1",
56
+ "'; DROP TABLE users; --"
39
57
  ];
40
58
  const expected = [
41
59
  'alert("xss")',
42
60
  'alert("xss")',
43
- 'Hello &amp; World'
61
+ 'Hello &amp; World',
62
+ "admin'' OR ''1''=''1",
63
+ "''; DROP TABLE users; --"
44
64
  ];
45
65
  expect(sanitizeObject(input)).toEqual(expected);
46
66
  });
@@ -49,14 +69,26 @@ describe('sanitizeObject', () => {
49
69
  name: ' <script>alert("xss")</script> ',
50
70
  address: {
51
71
  street: 'javascript:alert("xss")',
52
- city: 'Hello & World'
72
+ city: 'Hello & World',
73
+ country: "O'Connor",
74
+ postalCode: "123' OR '1'='1"
75
+ },
76
+ credentials: {
77
+ username: "admin' --",
78
+ password: "'; UPDATE users SET password='hacked'; --"
53
79
  }
54
80
  };
55
81
  const expected = {
56
82
  name: 'alert("xss")',
57
83
  address: {
58
84
  street: 'alert("xss")',
59
- city: 'Hello &amp; World'
85
+ city: 'Hello &amp; World',
86
+ country: "O''Connor",
87
+ postalCode: "123'' OR ''1''=''1"
88
+ },
89
+ credentials: {
90
+ username: "admin'' --",
91
+ password: "''; UPDATE users SET password=''hacked''; --"
60
92
  }
61
93
  };
62
94
  expect(sanitizeObject(input)).toEqual(expected);
@@ -64,11 +96,19 @@ describe('sanitizeObject', () => {
64
96
  it('handles arrays of objects', () => {
65
97
  const input = [
66
98
  { name: '<script>alert("xss")</script>' },
67
- { address: 'javascript:alert("xss")' }
99
+ { address: 'javascript:alert("xss")' },
100
+ {
101
+ username: "admin' OR '1'='1",
102
+ query: "'; DROP TABLE users; --"
103
+ }
68
104
  ];
69
105
  const expected = [
70
106
  { name: 'alert("xss")' },
71
- { address: 'alert("xss")' }
107
+ { address: 'alert("xss")' },
108
+ {
109
+ username: "admin'' OR ''1''=''1",
110
+ query: "''; DROP TABLE users; --"
111
+ }
72
112
  ];
73
113
  expect(sanitizeObject(input)).toEqual(expected);
74
114
  });
@@ -77,7 +117,12 @@ describe('sanitizeObject', () => {
77
117
  level1: {
78
118
  level2: {
79
119
  level3: {
80
- value: '<script>alert("xss")</script>'
120
+ value: '<script>alert("xss")</script>',
121
+ sqlInjection: {
122
+ query: "'; DROP TABLE users; --",
123
+ condition: "admin' OR '1'='1",
124
+ update: "'; UPDATE users SET password='hacked'; --"
125
+ }
81
126
  }
82
127
  }
83
128
  }
@@ -86,7 +131,12 @@ describe('sanitizeObject', () => {
86
131
  level1: {
87
132
  level2: {
88
133
  level3: {
89
- value: 'alert("xss")'
134
+ value: 'alert("xss")',
135
+ sqlInjection: {
136
+ query: "''; DROP TABLE users; --",
137
+ condition: "admin'' OR ''1''=''1",
138
+ update: "''; UPDATE users SET password=''hacked''; --"
139
+ }
90
140
  }
91
141
  }
92
142
  }
@@ -107,12 +157,17 @@ describe('sanitizeObject', () => {
107
157
  array: [
108
158
  'javascript:alert("xss")',
109
159
  456,
110
- false
160
+ false,
161
+ "admin' OR '1'='1"
111
162
  ],
112
163
  object: {
113
164
  string: 'Hello & World',
114
165
  number: 789,
115
- boolean: false
166
+ boolean: false,
167
+ sqlInjection: {
168
+ query: "'; DROP TABLE users; --",
169
+ username: "admin' --"
170
+ }
116
171
  }
117
172
  };
118
173
  const expected = {
@@ -124,12 +179,17 @@ describe('sanitizeObject', () => {
124
179
  array: [
125
180
  'alert("xss")',
126
181
  456,
127
- false
182
+ false,
183
+ "admin'' OR ''1''=''1"
128
184
  ],
129
185
  object: {
130
186
  string: 'Hello &amp; World',
131
187
  number: 789,
132
- boolean: false
188
+ boolean: false,
189
+ sqlInjection: {
190
+ query: "''; DROP TABLE users; --",
191
+ username: "admin'' --"
192
+ }
133
193
  }
134
194
  };
135
195
  expect(sanitizeObject(input)).toEqual(expected);
@@ -3,7 +3,7 @@ import "dotenv/config";
3
3
  const client_id = process.env.XERO_CLIENT_ID;
4
4
  const client_secret = process.env.XERO_CLIENT_SECRET;
5
5
  const redirectUrl = process.env.XERO_REDIRECT_URI;
6
- const scopes = "offline_access openid profile accounting.transactions.read accounting.contacts.read accounting.journals.read accounting.reports.read";
6
+ const scopes = "offline_access openid profile accounting.settings accounting.contacts accounting.invoices accounting.banktransactions accounting.payments.read accounting.reports.balancesheet.read";
7
7
  if (!client_id || !client_secret || !redirectUrl) {
8
8
  throw Error("Environment Variables not all set - please check your .env file in the project root or create one!");
9
9
  }
@@ -1,9 +1,11 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
2
+ import { CallToolRequestSchema, ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { McpToolsFactory } from "./Tools/McpToolsFactory.js";
5
5
  import { XeroAuthMiddleware } from "./Middlewares/XeroAuthMiddleware.js";
6
6
  import { ErrorMiddleware } from "./Middlewares/ErrorMiddleware.js";
7
+ import { XeroAccountingApiSchema } from "./Resources/xero_accounting.js";
8
+ const ACCOUNTING_OPENAPI_RESOURCE_URI = "xero-mcp://accounting/openapi.json";
7
9
  export class XeroMcpServer {
8
10
  mcpServer;
9
11
  constructor() {
@@ -13,11 +15,13 @@ export class XeroMcpServer {
13
15
  }, {
14
16
  capabilities: {
15
17
  tools: {},
18
+ resources: {},
16
19
  },
17
20
  });
18
21
  }
19
22
  async start() {
20
23
  this.configureTools();
24
+ this.configureResources();
21
25
  const transport = new StdioServerTransport();
22
26
  await this.mcpServer.connect(transport);
23
27
  console.error("Xero MCP server running on stdio");
@@ -50,4 +54,31 @@ export class XeroMcpServer {
50
54
  });
51
55
  });
52
56
  }
57
+ configureResources() {
58
+ this.mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
59
+ resources: [
60
+ {
61
+ uri: ACCOUNTING_OPENAPI_RESOURCE_URI,
62
+ name: "Xero Accounting API (OpenAPI)",
63
+ description: "OpenAPI 3.0 document for the Xero Accounting API (paths, operations, schemas). Use when you need request/response shapes or endpoint details beyond the bundled tools.",
64
+ mimeType: "application/json",
65
+ },
66
+ ],
67
+ }));
68
+ this.mcpServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [] }));
69
+ this.mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
70
+ if (request.params.uri !== ACCOUNTING_OPENAPI_RESOURCE_URI) {
71
+ throw new McpError(ErrorCode.InvalidParams, `Resource not found: ${request.params.uri}`);
72
+ }
73
+ return {
74
+ contents: [
75
+ {
76
+ uri: ACCOUNTING_OPENAPI_RESOURCE_URI,
77
+ mimeType: "application/json",
78
+ text: JSON.stringify(XeroAccountingApiSchema),
79
+ },
80
+ ],
81
+ };
82
+ });
83
+ }
53
84
  }
package/build/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "xero-mcp",
3
- "version": "1.3.0",
4
- "description": "A Model Context Protocol server allows Clients to interact with Xero",
5
- "author": "Jianyang Zhang",
6
- "type": "module",
7
- "main": "build/index.js",
8
- "bin": {
9
- "xero-mcp": "./build/index.js"
10
- },
11
- "scripts": {
12
- "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
13
- "start:dev": "tsx src/index.ts",
14
- "test": "jest"
15
- },
16
- "files": [
17
- "build",
18
- "README.md"
19
- ],
20
- "keywords": [
21
- "mcp",
22
- "xero",
23
- "modelcontextprotocol",
24
- "AI",
25
- "accounting"
26
- ],
27
- "repository": {
28
- "type": "git",
29
- "url": "git+https://github.com/john-zhang-dev/xero-mcp.git"
30
- },
31
- "dependencies": {
32
- "@modelcontextprotocol/sdk": "^1.7.0",
33
- "dotenv": "^16.4.7",
34
- "open": "^10.1.0",
35
- "xero-node": "^10.0.0",
36
- "zod": "^3.24.2"
37
- },
38
- "devDependencies": {
39
- "@types/jest": "^29.5.14",
40
- "@types/node": "^22.13.10",
41
- "jest": "^29.7.0",
42
- "ts-jest": "^29.3.0",
43
- "ts-node": "^10.9.2",
44
- "tsx": "^4.19.3",
45
- "typescript": "^5.8.2"
46
- },
47
- "license": "MIT"
1
+ {
2
+ "name": "xero-mcp",
3
+ "version": "2.0.0-beta",
4
+ "description": "A Model Context Protocol server allows Clients to interact with Xero",
5
+ "author": "Jianyang Zhang",
6
+ "type": "module",
7
+ "main": "build/index.js",
8
+ "bin": {
9
+ "xero-mcp": "./build/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
13
+ "start:dev": "tsx src/index.ts",
14
+ "test": "jest"
15
+ },
16
+ "files": [
17
+ "build",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "mcp",
22
+ "xero",
23
+ "modelcontextprotocol",
24
+ "AI",
25
+ "accounting"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/john-zhang-dev/xero-mcp.git"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.7.0",
33
+ "dotenv": "^16.4.7",
34
+ "open": "^10.1.0",
35
+ "xero-node": "^14.0.0",
36
+ "zod": "^3.24.2"
37
+ },
38
+ "devDependencies": {
39
+ "@types/jest": "^29.5.14",
40
+ "@types/node": "^25.5.0",
41
+ "jest": "^29.7.0",
42
+ "ts-jest": "^29.3.0",
43
+ "ts-node": "^10.9.2",
44
+ "tsx": "^4.19.3",
45
+ "typescript": "^5.8.2"
46
+ },
47
+ "license": "MIT"
48
48
  }