mcp-intervals 1.5.0 → 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/cli/api.js +1 -1
- package/dist/cli/init.js +0 -0
- package/dist/client.d.ts +24 -16
- package/dist/client.js +75 -32
- package/dist/index.js +6 -2
- 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/dist/tools.js +40 -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/cli/api.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function validateToken(token) {
|
|
|
11
11
|
const data = (await response.json());
|
|
12
12
|
return {
|
|
13
13
|
valid: true,
|
|
14
|
-
workspace: data.me?.company || "Unknown workspace",
|
|
14
|
+
workspace: data.me?.[0]?.company || "Unknown workspace",
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
if (response.status === 401) {
|
package/dist/cli/init.js
CHANGED
|
File without changes
|
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";
|
|
@@ -27,31 +38,54 @@ export class IntervalsClient {
|
|
|
27
38
|
const response = await fetch(url, fetchOptions);
|
|
28
39
|
if (!response.ok) {
|
|
29
40
|
const text = await response.text();
|
|
30
|
-
throw new Error(`Intervals API error ${response.status}: ${text}`);
|
|
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,16 @@ 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
|
+
}
|
|
168
|
+
if (typeof personid !== "number") {
|
|
169
|
+
throw new Error("Could not determine current user's person ID from Intervals /me/ response.");
|
|
170
|
+
}
|
|
130
171
|
const data = await this.request(`/time/`, {
|
|
131
172
|
method: "POST",
|
|
132
173
|
body: {
|
|
@@ -136,19 +177,21 @@ export class IntervalsClient {
|
|
|
136
177
|
date: fields.date,
|
|
137
178
|
time: fields.time,
|
|
138
179
|
billable: fields.billable,
|
|
180
|
+
...(fields.moduleid !== undefined && { moduleid: fields.moduleid }),
|
|
139
181
|
...(fields.description && { description: fields.description }),
|
|
140
182
|
},
|
|
183
|
+
...notify,
|
|
141
184
|
});
|
|
142
185
|
return data;
|
|
143
186
|
}
|
|
144
187
|
async getTimeEntries(params = {}) {
|
|
145
|
-
const data = await this.request(`/time/`, { params
|
|
188
|
+
const data = await this.request(`/time/`, { params });
|
|
146
189
|
return data;
|
|
147
190
|
}
|
|
148
191
|
// --- Me (current user) ---
|
|
149
192
|
async getMe() {
|
|
150
193
|
const data = await this.request(`/me/`);
|
|
151
194
|
// Return the first user object with personid from the top-level response
|
|
152
|
-
return { ...data.me[0], personid: data.personid };
|
|
195
|
+
return { ...(data.me?.[0] ?? {}), personid: data.personid };
|
|
153
196
|
}
|
|
154
197
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,12 @@ 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
|
+
const { createRequire } = await import("node:module");
|
|
17
|
+
// Read version from package.json so it never drifts from the published one.
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { version } = require("../package.json");
|
|
16
20
|
const API_TOKEN = process.env.INTERVALS_API_TOKEN;
|
|
17
21
|
if (!API_TOKEN) {
|
|
18
22
|
console.error("Error: INTERVALS_API_TOKEN environment variable is required.");
|
|
@@ -21,7 +25,7 @@ async function startServer() {
|
|
|
21
25
|
const client = new IntervalsClient(API_TOKEN);
|
|
22
26
|
const server = new McpServer({
|
|
23
27
|
name: "mcp-intervals",
|
|
24
|
-
version
|
|
28
|
+
version,
|
|
25
29
|
});
|
|
26
30
|
registerTools(server, client);
|
|
27
31
|
registerResources(server, client);
|
|
@@ -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
|
+
}
|