mcp-intervals 1.5.1 → 1.6.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/README.md +37 -10
- package/dist/client.d.ts +24 -16
- package/dist/client.js +70 -30
- package/dist/index.js +1 -1
- package/dist/tools/billing.d.ts +3 -0
- package/dist/tools/billing.js +243 -0
- package/dist/tools/core.d.ts +3 -0
- package/dist/tools/core.js +430 -0
- package/dist/tools/documents.d.ts +3 -0
- package/dist/tools/documents.js +51 -0
- package/dist/tools/helpers.d.ts +49 -0
- package/dist/tools/helpers.js +80 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/people.d.ts +3 -0
- package/dist/tools/people.js +177 -0
- package/dist/tools/projects.d.ts +3 -0
- package/dist/tools/projects.js +497 -0
- package/dist/tools/tasks.d.ts +9 -0
- package/dist/tools/tasks.js +171 -0
- package/dist/tools/time.d.ts +3 -0
- package/dist/tools/time.js +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,16 +111,43 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
111
111
|
|
|
112
112
|
## Available Tools
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
114
|
+
104 tools covering the full Intervals API. Tasks are addressed by the **local task ID** shown in the web UI (or a task URL for `get_task`); every other record uses its numeric API ID.
|
|
115
|
+
|
|
116
|
+
**Permissions:** some collection endpoints (`worktype`, `invoice`, `expense`, `module`, `payment`) return 403 depending on the user's permission group. Project-scoped alternatives exist: `get_project_worktypes` and `get_project_modules`.
|
|
117
|
+
|
|
118
|
+
| Area | Tools |
|
|
119
|
+
| ---- | ----- |
|
|
120
|
+
| Tasks | `get_task`, `get_tasks`, `create_task`, `update_task`, `delete_task`, `get_task_statuses`, `get_task_priorities` |
|
|
121
|
+
| Task notes | `get_task_notes`, `add_task_note`, `update_task_note`, `delete_task_note` |
|
|
122
|
+
| Task filters | `get_task_filters`, `get_task_filter`, `create_task_filter`, `update_task_filter` |
|
|
123
|
+
| Time | `add_time_entry`, `get_time_entries`, `update_time_entry`, `delete_time_entry`, `get_worktypes` |
|
|
124
|
+
| Timers | `get_timers`, `start_timer`, `stop_timer` |
|
|
125
|
+
| Expenses | `get_expenses`, `create_expense` |
|
|
126
|
+
| Projects | `get_project`, `get_projects`, `create_project`, `update_project` |
|
|
127
|
+
| Project team | `get_project_team`, `add_project_team_member`, `remove_project_team_member` |
|
|
128
|
+
| Project modules | `get_project_modules`, `add_project_module`, `update_project_module`, `remove_project_module` |
|
|
129
|
+
| Project work types | `get_project_worktypes`, `add_project_worktype`, `update_project_worktype`, `remove_project_worktype` |
|
|
130
|
+
| Project labels | `get_project_labels`, `get_project_label`, `create_project_label`, `update_project_label` |
|
|
131
|
+
| Project notes | `get_project_notes`, `get_project_note`, `create_project_note`, `update_project_note`, `delete_project_note` |
|
|
132
|
+
| Default modules | `get_modules`, `get_module`, `create_module`, `update_module` |
|
|
133
|
+
| Milestones | `get_milestone`, `get_milestones`, `create_milestone`, `update_milestone` |
|
|
134
|
+
| Milestone notes | `get_milestone_notes`, `create_milestone_note`, `update_milestone_note`, `delete_milestone_note` |
|
|
135
|
+
| Requests | `get_requests`, `get_request`, `create_request`, `update_request`, `delete_request` |
|
|
136
|
+
| Clients | `get_clients`, `get_client`, `create_client`, `update_client` |
|
|
137
|
+
| People | `get_people`, `get_person`, `get_me`, `get_groups`, `get_quota` |
|
|
138
|
+
| Contacts | `get_contact_types`, `get_contact_descriptors`, `get_person_contacts`, `create_person_contact`, `update_person_contact`, `delete_person_contact` |
|
|
139
|
+
| Invoices | `get_invoices`, `get_invoice`, `create_invoice`, `get_invoice_terms` |
|
|
140
|
+
| Invoice items | `get_invoice_items`, `create_invoice_item`, `update_invoice_item`, `delete_invoice_item` |
|
|
141
|
+
| Invoice notes | `get_invoice_notes`, `create_invoice_note`, `update_invoice_note`, `delete_invoice_note` |
|
|
142
|
+
| Payments | `get_payments`, `get_payment`, `create_payment`, `update_payment`, `delete_payment`, `get_payment_types` |
|
|
143
|
+
| Documents | `get_documents`, `get_document`, `download_document` (images inline, PDF text), `create_document`, `update_document` |
|
|
144
|
+
|
|
145
|
+
### Notifications
|
|
146
|
+
|
|
147
|
+
Every write tool (create/update/delete) accepts two optional booleans:
|
|
148
|
+
|
|
149
|
+
- `send_notifications` - sends `X-Intervals-Send-Notifications: t`. **Emails are not sent unless this is true** (the API default).
|
|
150
|
+
- `disable_action_notes` - sends `X-Intervals-Disable-Action-Notes: t` to suppress the automatic action notes Intervals adds to tasks and milestones.
|
|
124
151
|
|
|
125
152
|
## Resources
|
|
126
153
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type QueryParams = Record<string, string | number | boolean | undefined>;
|
|
2
|
+
/**
|
|
3
|
+
* Opt-in notification controls for write requests. The Intervals API sends no
|
|
4
|
+
* emails by default, so nothing is added unless a flag is explicitly true.
|
|
5
|
+
*/
|
|
6
|
+
export interface NotificationOptions {
|
|
7
|
+
/** Maps to `X-Intervals-Send-Notifications: t` (emails are NOT sent otherwise). */
|
|
8
|
+
sendNotifications?: boolean;
|
|
9
|
+
/** Maps to `X-Intervals-Disable-Action-Notes: t` (suppress automatic action notes). */
|
|
10
|
+
disableActionNotes?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface IntervalsRequestOptions extends NotificationOptions {
|
|
2
13
|
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
3
14
|
body?: Record<string, unknown>;
|
|
4
|
-
params?:
|
|
15
|
+
params?: QueryParams;
|
|
5
16
|
}
|
|
6
17
|
export declare class IntervalsClient {
|
|
7
18
|
private baseUrl;
|
|
@@ -9,19 +20,19 @@ export declare class IntervalsClient {
|
|
|
9
20
|
constructor(apiToken: string);
|
|
10
21
|
private request;
|
|
11
22
|
private requestBinary;
|
|
23
|
+
get(path: string, params?: QueryParams): Promise<Record<string, unknown>>;
|
|
24
|
+
post(path: string, body: Record<string, unknown>, notify?: NotificationOptions): Promise<Record<string, unknown>>;
|
|
25
|
+
put(path: string, body: Record<string, unknown>, notify?: NotificationOptions): Promise<Record<string, unknown>>;
|
|
26
|
+
delete(path: string, params?: QueryParams, notify?: NotificationOptions): Promise<Record<string, unknown>>;
|
|
12
27
|
getTask(id: number): Promise<Record<string, unknown>>;
|
|
13
28
|
getTaskByLocalId(localId: number): Promise<Record<string, unknown>>;
|
|
14
29
|
resolveTaskId(localId: number): Promise<number>;
|
|
15
|
-
updateTask(id: number, fields: Record<string, unknown
|
|
16
|
-
getTaskNotes(taskId: number): Promise<Record<string, unknown>>;
|
|
17
|
-
addTaskNote(taskId: number, note: string, isPublic?: boolean): Promise<Record<string, unknown>>;
|
|
30
|
+
updateTask(id: number, fields: Record<string, unknown>, notify?: NotificationOptions): Promise<Record<string, unknown>>;
|
|
31
|
+
getTaskNotes(taskId: number, filters?: QueryParams): Promise<Record<string, unknown>>;
|
|
32
|
+
addTaskNote(taskId: number, note: string, isPublic?: boolean, notify?: NotificationOptions): Promise<Record<string, unknown>>;
|
|
18
33
|
getProject(id: number): Promise<Record<string, unknown>>;
|
|
19
34
|
getMilestone(id: number): Promise<Record<string, unknown>>;
|
|
20
|
-
getDocuments(params?:
|
|
21
|
-
taskid?: number;
|
|
22
|
-
projectid?: number;
|
|
23
|
-
personid?: number;
|
|
24
|
-
}): Promise<Record<string, unknown>>;
|
|
35
|
+
getDocuments(params?: QueryParams): Promise<Record<string, unknown>>;
|
|
25
36
|
getDocument(id: number): Promise<Record<string, unknown>>;
|
|
26
37
|
downloadDocument(id: number): Promise<{
|
|
27
38
|
buffer: ArrayBuffer;
|
|
@@ -37,13 +48,10 @@ export declare class IntervalsClient {
|
|
|
37
48
|
time: number;
|
|
38
49
|
billable: boolean;
|
|
39
50
|
description?: string;
|
|
40
|
-
}): Promise<Record<string, unknown>>;
|
|
41
|
-
getTimeEntries(params?: {
|
|
42
|
-
taskid?: number;
|
|
43
51
|
personid?: number;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
moduleid?: number;
|
|
53
|
+
}, notify?: NotificationOptions): Promise<Record<string, unknown>>;
|
|
54
|
+
getTimeEntries(params?: QueryParams): Promise<Record<string, unknown>>;
|
|
47
55
|
getMe(): Promise<{
|
|
48
56
|
personid: number;
|
|
49
57
|
}>;
|
package/dist/client.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
function buildQueryString(params) {
|
|
2
|
+
if (!params)
|
|
3
|
+
return "";
|
|
4
|
+
const searchParams = new URLSearchParams();
|
|
5
|
+
for (const [key, value] of Object.entries(params)) {
|
|
6
|
+
if (value === undefined)
|
|
7
|
+
continue;
|
|
8
|
+
searchParams.set(key, String(value));
|
|
9
|
+
}
|
|
10
|
+
const query = searchParams.toString();
|
|
11
|
+
return query ? `?${query}` : "";
|
|
12
|
+
}
|
|
1
13
|
export class IntervalsClient {
|
|
2
14
|
baseUrl = "https://api.myintervals.com";
|
|
3
15
|
authHeader;
|
|
@@ -6,19 +18,18 @@ export class IntervalsClient {
|
|
|
6
18
|
"Basic " + Buffer.from(`${apiToken}:X`).toString("base64");
|
|
7
19
|
}
|
|
8
20
|
async request(path, options = {}) {
|
|
9
|
-
const { method = "GET", body, params } = options;
|
|
10
|
-
|
|
11
|
-
if (params) {
|
|
12
|
-
const searchParams = new URLSearchParams();
|
|
13
|
-
for (const [key, value] of Object.entries(params)) {
|
|
14
|
-
searchParams.set(key, String(value));
|
|
15
|
-
}
|
|
16
|
-
url += `?${searchParams.toString()}`;
|
|
17
|
-
}
|
|
21
|
+
const { method = "GET", body, params, sendNotifications, disableActionNotes, } = options;
|
|
22
|
+
const url = `${this.baseUrl}${path}${buildQueryString(params)}`;
|
|
18
23
|
const headers = {
|
|
19
24
|
Authorization: this.authHeader,
|
|
20
25
|
Accept: "application/json",
|
|
21
26
|
};
|
|
27
|
+
if (sendNotifications) {
|
|
28
|
+
headers["X-Intervals-Send-Notifications"] = "t";
|
|
29
|
+
}
|
|
30
|
+
if (disableActionNotes) {
|
|
31
|
+
headers["X-Intervals-Disable-Action-Notes"] = "t";
|
|
32
|
+
}
|
|
22
33
|
const fetchOptions = { method, headers };
|
|
23
34
|
if (body) {
|
|
24
35
|
headers["Content-Type"] = "application/json";
|
|
@@ -29,29 +40,52 @@ export class IntervalsClient {
|
|
|
29
40
|
const text = await response.text();
|
|
30
41
|
throw new Error(`Intervals API error ${response.status}: ${text.slice(0, 500)}`);
|
|
31
42
|
}
|
|
32
|
-
return
|
|
43
|
+
// Successful writes (e.g. DELETE) may return an empty body.
|
|
44
|
+
const text = await response.text();
|
|
45
|
+
if (!text.trim())
|
|
46
|
+
return {};
|
|
47
|
+
return JSON.parse(text);
|
|
33
48
|
}
|
|
34
49
|
async requestBinary(path, params) {
|
|
35
|
-
|
|
36
|
-
if (params) {
|
|
37
|
-
const searchParams = new URLSearchParams();
|
|
38
|
-
for (const [key, value] of Object.entries(params)) {
|
|
39
|
-
searchParams.set(key, String(value));
|
|
40
|
-
}
|
|
41
|
-
url += `?${searchParams.toString()}`;
|
|
42
|
-
}
|
|
50
|
+
const url = `${this.baseUrl}${path}${buildQueryString(params)}`;
|
|
43
51
|
const response = await fetch(url, {
|
|
44
52
|
method: "GET",
|
|
45
53
|
headers: { Authorization: this.authHeader },
|
|
46
54
|
});
|
|
47
55
|
if (!response.ok) {
|
|
48
56
|
const text = await response.text();
|
|
49
|
-
throw new Error(`Intervals API error ${response.status}: ${text}`);
|
|
57
|
+
throw new Error(`Intervals API error ${response.status}: ${text.slice(0, 500)}`);
|
|
50
58
|
}
|
|
51
59
|
const buffer = await response.arrayBuffer();
|
|
52
60
|
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
53
61
|
return { buffer, contentType };
|
|
54
62
|
}
|
|
63
|
+
// --- Generic verbs (used by the declarative endpoint tools) ---
|
|
64
|
+
// Paths are given without the trailing slash, e.g. "/client" or "/client/12".
|
|
65
|
+
get(path, params) {
|
|
66
|
+
return this.request(`${path}/`, { params });
|
|
67
|
+
}
|
|
68
|
+
post(path, body, notify = {}) {
|
|
69
|
+
return this.request(`${path}/`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body,
|
|
72
|
+
...notify,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
put(path, body, notify = {}) {
|
|
76
|
+
return this.request(`${path}/`, {
|
|
77
|
+
method: "PUT",
|
|
78
|
+
body,
|
|
79
|
+
...notify,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
delete(path, params, notify = {}) {
|
|
83
|
+
return this.request(`${path}/`, {
|
|
84
|
+
method: "DELETE",
|
|
85
|
+
params,
|
|
86
|
+
...notify,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
55
89
|
// --- Task ---
|
|
56
90
|
async getTask(id) {
|
|
57
91
|
const data = await this.request(`/task/${id}/`);
|
|
@@ -68,19 +102,20 @@ export class IntervalsClient {
|
|
|
68
102
|
const task = await this.getTaskByLocalId(localId);
|
|
69
103
|
return Number(task.id);
|
|
70
104
|
}
|
|
71
|
-
async updateTask(id, fields) {
|
|
72
|
-
const data = await this.request(`/task/${id}/`, { method: "PUT", body: fields });
|
|
105
|
+
async updateTask(id, fields, notify = {}) {
|
|
106
|
+
const data = await this.request(`/task/${id}/`, { method: "PUT", body: fields, ...notify });
|
|
73
107
|
return data;
|
|
74
108
|
}
|
|
75
109
|
// --- Task Notes ---
|
|
76
|
-
async getTaskNotes(taskId) {
|
|
77
|
-
const data = await this.request(`/tasknote/`, { params: { taskid: taskId } });
|
|
110
|
+
async getTaskNotes(taskId, filters = {}) {
|
|
111
|
+
const data = await this.request(`/tasknote/`, { params: { ...filters, taskid: taskId } });
|
|
78
112
|
return data;
|
|
79
113
|
}
|
|
80
|
-
async addTaskNote(taskId, note, isPublic = true) {
|
|
114
|
+
async addTaskNote(taskId, note, isPublic = true, notify = {}) {
|
|
81
115
|
const data = await this.request(`/tasknote/`, {
|
|
82
116
|
method: "POST",
|
|
83
117
|
body: { taskid: taskId, note, public: isPublic },
|
|
118
|
+
...notify,
|
|
84
119
|
});
|
|
85
120
|
return data;
|
|
86
121
|
}
|
|
@@ -97,7 +132,7 @@ export class IntervalsClient {
|
|
|
97
132
|
// --- Documents ---
|
|
98
133
|
async getDocuments(params = {}) {
|
|
99
134
|
const data = await this.request(`/document/`, {
|
|
100
|
-
params
|
|
135
|
+
params,
|
|
101
136
|
});
|
|
102
137
|
return data;
|
|
103
138
|
}
|
|
@@ -123,10 +158,13 @@ export class IntervalsClient {
|
|
|
123
158
|
return data;
|
|
124
159
|
}
|
|
125
160
|
// --- Time Entries ---
|
|
126
|
-
async addTimeEntry(fields) {
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
161
|
+
async addTimeEntry(fields, notify = {}) {
|
|
162
|
+
// Default to the current user's person ID
|
|
163
|
+
let personid = fields.personid;
|
|
164
|
+
if (personid === undefined) {
|
|
165
|
+
const me = await this.getMe();
|
|
166
|
+
personid = me.personid;
|
|
167
|
+
}
|
|
130
168
|
if (typeof personid !== "number") {
|
|
131
169
|
throw new Error("Could not determine current user's person ID from Intervals /me/ response.");
|
|
132
170
|
}
|
|
@@ -139,13 +177,15 @@ export class IntervalsClient {
|
|
|
139
177
|
date: fields.date,
|
|
140
178
|
time: fields.time,
|
|
141
179
|
billable: fields.billable,
|
|
180
|
+
...(fields.moduleid !== undefined && { moduleid: fields.moduleid }),
|
|
142
181
|
...(fields.description && { description: fields.description }),
|
|
143
182
|
},
|
|
183
|
+
...notify,
|
|
144
184
|
});
|
|
145
185
|
return data;
|
|
146
186
|
}
|
|
147
187
|
async getTimeEntries(params = {}) {
|
|
148
|
-
const data = await this.request(`/time/`, { params
|
|
188
|
+
const data = await this.request(`/time/`, { params });
|
|
149
189
|
return data;
|
|
150
190
|
}
|
|
151
191
|
// --- Me (current user) ---
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ async function startServer() {
|
|
|
11
11
|
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
12
12
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
13
|
const { IntervalsClient } = await import("./client.js");
|
|
14
|
-
const { registerTools } = await import("./tools.js");
|
|
14
|
+
const { registerTools } = await import("./tools/index.js");
|
|
15
15
|
const { registerResources } = await import("./resources.js");
|
|
16
16
|
const { createRequire } = await import("node:module");
|
|
17
17
|
// Read version from package.json so it never drifts from the published one.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { limitParam, offsetParam, registerEndpoints } from "./helpers.js";
|
|
3
|
+
const itemFields = {
|
|
4
|
+
invoiceid: z.number().describe("Invoice ID (use get_invoices)"),
|
|
5
|
+
name: z.string().describe("Line item name (max 255 characters)"),
|
|
6
|
+
amount: z.number().describe("Line item amount"),
|
|
7
|
+
description: z.string().describe("Line item description"),
|
|
8
|
+
quantity: z.number().describe("Quantity"),
|
|
9
|
+
rate: z.number().describe("Rate"),
|
|
10
|
+
};
|
|
11
|
+
const invoiceNoteFields = {
|
|
12
|
+
invoiceid: z.number().describe("Invoice ID (use get_invoices)"),
|
|
13
|
+
title: z.string().describe("Note title (max 255 characters)"),
|
|
14
|
+
note: z.string().describe("Note body"),
|
|
15
|
+
date: z.string().describe("Note date (YYYY-MM-DD HH:MM:SS)"),
|
|
16
|
+
};
|
|
17
|
+
const paymentFields = {
|
|
18
|
+
projectid: z.number().describe("Project ID (use get_projects)"),
|
|
19
|
+
date: z.string().describe("Payment date (YYYY-MM-DD)"),
|
|
20
|
+
amount: z.number().describe("Payment amount"),
|
|
21
|
+
typeid: z.number().describe("Payment type ID (use get_payment_types)"),
|
|
22
|
+
invoiceid: z.number().describe("Invoice ID to apply the payment to (use get_invoices)"),
|
|
23
|
+
reference: z.string().describe("Reference number (max 35 characters)"),
|
|
24
|
+
note: z.string().describe("Payment note (max 255 characters)"),
|
|
25
|
+
};
|
|
26
|
+
const specs = [
|
|
27
|
+
// --- Invoices ---
|
|
28
|
+
{
|
|
29
|
+
name: "get_invoices",
|
|
30
|
+
description: "List invoices, optionally filtered by client or status (draft, sent, paid).",
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: "/invoice",
|
|
33
|
+
schema: {
|
|
34
|
+
clientid: z.number().optional().describe("Filter by client ID"),
|
|
35
|
+
status: z.string().optional().describe("Invoice status: draft, sent, or paid"),
|
|
36
|
+
limit: limitParam,
|
|
37
|
+
offset: offsetParam,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "get_invoice",
|
|
42
|
+
description: "Get full details of a single invoice by ID.",
|
|
43
|
+
method: "GET",
|
|
44
|
+
path: "/invoice",
|
|
45
|
+
idParam: "invoiceId",
|
|
46
|
+
schema: { invoiceId: z.number().describe("Invoice ID") },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "create_invoice",
|
|
50
|
+
description: "Create an invoice for a project. Requires project, date, term ID, and title.",
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "/invoice",
|
|
53
|
+
schema: {
|
|
54
|
+
projectid: z.number().describe("Project ID the invoice is for (use get_projects)"),
|
|
55
|
+
date: z.string().describe("Invoice date (YYYY-MM-DD)"),
|
|
56
|
+
termid: z.number().describe("Invoice term ID, e.g. Net 30 (use get_invoice_terms)"),
|
|
57
|
+
title: z.string().describe("Invoice title (max 255 characters)"),
|
|
58
|
+
description: z.string().optional().describe("Invoice description/notes"),
|
|
59
|
+
termother: z.string().optional().describe("Custom payment terms text (max 255 characters)"),
|
|
60
|
+
termotherdatedue: z.string().optional().describe("Custom due date when using termother (YYYY-MM-DD)"),
|
|
61
|
+
addressfrom: z.string().optional().describe("From address on the invoice"),
|
|
62
|
+
addressto: z.string().optional().describe("To address on the invoice"),
|
|
63
|
+
purchaseorder: z.string().optional().describe("Purchase order number (max 55 characters)"),
|
|
64
|
+
tax: z.number().optional().describe("Tax rate percentage"),
|
|
65
|
+
datebegin: z.string().optional().describe("Billing period start date (YYYY-MM-DD)"),
|
|
66
|
+
dateend: z.string().optional().describe("Billing period end date (YYYY-MM-DD)"),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "get_invoice_terms",
|
|
71
|
+
description: "List invoice payment terms (e.g. Net 30, Upon Receipt). Use the IDs when creating invoices.",
|
|
72
|
+
method: "GET",
|
|
73
|
+
path: "/invoiceterm",
|
|
74
|
+
schema: {},
|
|
75
|
+
},
|
|
76
|
+
// --- Invoice items ---
|
|
77
|
+
{
|
|
78
|
+
name: "get_invoice_items",
|
|
79
|
+
description: "List line items for an invoice.",
|
|
80
|
+
method: "GET",
|
|
81
|
+
path: "/invoiceitem",
|
|
82
|
+
schema: {
|
|
83
|
+
invoiceid: z.number().describe("Invoice ID to list items for"),
|
|
84
|
+
limit: limitParam,
|
|
85
|
+
offset: offsetParam,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "create_invoice_item",
|
|
90
|
+
description: "Add a line item to an invoice.",
|
|
91
|
+
method: "POST",
|
|
92
|
+
path: "/invoiceitem",
|
|
93
|
+
schema: {
|
|
94
|
+
invoiceid: itemFields.invoiceid,
|
|
95
|
+
name: itemFields.name,
|
|
96
|
+
amount: itemFields.amount,
|
|
97
|
+
description: itemFields.description.optional(),
|
|
98
|
+
quantity: itemFields.quantity.optional(),
|
|
99
|
+
rate: itemFields.rate.optional(),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "update_invoice_item",
|
|
104
|
+
description: "Update an invoice line item. Only custom line items support quantity, rate, and amount changes.",
|
|
105
|
+
method: "PUT",
|
|
106
|
+
path: "/invoiceitem",
|
|
107
|
+
idParam: "invoiceItemId",
|
|
108
|
+
schema: {
|
|
109
|
+
invoiceItemId: z.number().describe("Invoice item ID to update"),
|
|
110
|
+
invoiceid: itemFields.invoiceid.optional(),
|
|
111
|
+
name: itemFields.name.optional(),
|
|
112
|
+
amount: itemFields.amount.optional(),
|
|
113
|
+
description: itemFields.description.optional(),
|
|
114
|
+
quantity: itemFields.quantity.optional(),
|
|
115
|
+
rate: itemFields.rate.optional(),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "delete_invoice_item",
|
|
120
|
+
description: "Delete a custom invoice line item by ID. Permanently deletes; cannot be undone.",
|
|
121
|
+
method: "DELETE",
|
|
122
|
+
path: "/invoiceitem",
|
|
123
|
+
idParam: "invoiceItemId",
|
|
124
|
+
schema: { invoiceItemId: z.number().describe("Invoice item ID to delete") },
|
|
125
|
+
},
|
|
126
|
+
// --- Invoice notes ---
|
|
127
|
+
{
|
|
128
|
+
name: "get_invoice_notes",
|
|
129
|
+
description: "List invoice notes, optionally for a single invoice.",
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: "/invoicenote",
|
|
132
|
+
schema: {
|
|
133
|
+
invoiceid: z.number().optional().describe("Filter by invoice ID"),
|
|
134
|
+
limit: limitParam,
|
|
135
|
+
offset: offsetParam,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "create_invoice_note",
|
|
140
|
+
description: "Add a note to an invoice.",
|
|
141
|
+
method: "POST",
|
|
142
|
+
path: "/invoicenote",
|
|
143
|
+
schema: {
|
|
144
|
+
invoiceid: invoiceNoteFields.invoiceid,
|
|
145
|
+
title: invoiceNoteFields.title,
|
|
146
|
+
note: invoiceNoteFields.note.optional(),
|
|
147
|
+
date: invoiceNoteFields.date.optional(),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "update_invoice_note",
|
|
152
|
+
description: "Update an invoice note. Only provide fields you want to change.",
|
|
153
|
+
method: "PUT",
|
|
154
|
+
path: "/invoicenote",
|
|
155
|
+
idParam: "invoiceNoteId",
|
|
156
|
+
schema: {
|
|
157
|
+
invoiceNoteId: z.number().describe("Invoice note ID to update"),
|
|
158
|
+
invoiceid: invoiceNoteFields.invoiceid.optional(),
|
|
159
|
+
title: invoiceNoteFields.title.optional(),
|
|
160
|
+
note: invoiceNoteFields.note.optional(),
|
|
161
|
+
date: invoiceNoteFields.date.optional(),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "delete_invoice_note",
|
|
166
|
+
description: "Delete an invoice note by ID. Permanently deletes; cannot be undone.",
|
|
167
|
+
method: "DELETE",
|
|
168
|
+
path: "/invoicenote",
|
|
169
|
+
idParam: "invoiceNoteId",
|
|
170
|
+
schema: { invoiceNoteId: z.number().describe("Invoice note ID to delete") },
|
|
171
|
+
},
|
|
172
|
+
// --- Payments ---
|
|
173
|
+
{
|
|
174
|
+
name: "get_payments",
|
|
175
|
+
description: "List payments, optionally filtered by project or invoice.",
|
|
176
|
+
method: "GET",
|
|
177
|
+
path: "/payment",
|
|
178
|
+
schema: {
|
|
179
|
+
projectid: z.number().optional().describe("Filter by project ID"),
|
|
180
|
+
invoiceid: z.number().optional().describe("Filter by invoice ID"),
|
|
181
|
+
limit: limitParam,
|
|
182
|
+
offset: offsetParam,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "get_payment",
|
|
187
|
+
description: "Get full details of a single payment by ID.",
|
|
188
|
+
method: "GET",
|
|
189
|
+
path: "/payment",
|
|
190
|
+
idParam: "paymentId",
|
|
191
|
+
schema: { paymentId: z.number().describe("Payment ID") },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "create_payment",
|
|
195
|
+
description: "Record a payment against a project.",
|
|
196
|
+
method: "POST",
|
|
197
|
+
path: "/payment",
|
|
198
|
+
schema: {
|
|
199
|
+
projectid: paymentFields.projectid,
|
|
200
|
+
date: paymentFields.date,
|
|
201
|
+
amount: paymentFields.amount,
|
|
202
|
+
typeid: paymentFields.typeid,
|
|
203
|
+
invoiceid: paymentFields.invoiceid.optional(),
|
|
204
|
+
reference: paymentFields.reference.optional(),
|
|
205
|
+
note: paymentFields.note.optional(),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "update_payment",
|
|
210
|
+
description: "Update a payment. Only provide fields you want to change.",
|
|
211
|
+
method: "PUT",
|
|
212
|
+
path: "/payment",
|
|
213
|
+
idParam: "paymentId",
|
|
214
|
+
schema: {
|
|
215
|
+
paymentId: z.number().describe("Payment ID to update"),
|
|
216
|
+
projectid: paymentFields.projectid.optional(),
|
|
217
|
+
date: paymentFields.date.optional(),
|
|
218
|
+
amount: paymentFields.amount.optional(),
|
|
219
|
+
typeid: paymentFields.typeid.optional(),
|
|
220
|
+
invoiceid: paymentFields.invoiceid.optional(),
|
|
221
|
+
reference: paymentFields.reference.optional(),
|
|
222
|
+
note: paymentFields.note.optional(),
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "delete_payment",
|
|
227
|
+
description: "Delete a payment by ID (Administrator only). Permanently deletes; cannot be undone.",
|
|
228
|
+
method: "DELETE",
|
|
229
|
+
path: "/payment",
|
|
230
|
+
idParam: "paymentId",
|
|
231
|
+
schema: { paymentId: z.number().describe("Payment ID to delete") },
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "get_payment_types",
|
|
235
|
+
description: "List payment types (e.g. Cash, Check, Credit Card). Use the IDs when creating payments.",
|
|
236
|
+
method: "GET",
|
|
237
|
+
path: "/paymenttype",
|
|
238
|
+
schema: {},
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
export function registerBillingTools(server, client) {
|
|
242
|
+
registerEndpoints(server, client, specs);
|
|
243
|
+
}
|