mcp-costlocker 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/README.md +72 -0
- package/dist/index.js +87 -0
- package/dist/integrations/costlocker/client.js +58 -0
- package/dist/integrations/costlocker/generated/client.js +1492 -0
- package/dist/integrations/costlocker/generated/types.js +1027 -0
- package/dist/integrations/costlocker/mutations/updateCostlockerEntry.js +50 -0
- package/dist/integrations/costlocker/queries/getCostlockerEntries.js +66 -0
- package/dist/tools/finance.js +62 -0
- package/dist/tools/lookup.js +65 -0
- package/dist/tools/people.js +55 -0
- package/dist/tools/projects.js +217 -0
- package/dist/tools/registerCostlockerGraphTools.js +12 -0
- package/dist/tools/shared.js +27 -0
- package/dist/tools/timesheet.js +206 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
## MCP Configuration
|
|
2
|
+
|
|
3
|
+
This project provides a Model Context Protocol (MCP) server that exposes two tools:
|
|
4
|
+
|
|
5
|
+
- `costlocker_get_time_entries`
|
|
6
|
+
- `costlocker_update_time_entries`
|
|
7
|
+
|
|
8
|
+
These tools interact with your Costlocker GraphQL API. You need to provide the `COSTLOCKER_API_KEY` in the environment.
|
|
9
|
+
|
|
10
|
+
### From npm (no local clone)
|
|
11
|
+
|
|
12
|
+
After the package is published to npm as `mcp-costlocker` (unscoped, public by default), use `npx` so Node downloads the package on first run. You do not need `pnpm` or a checkout of this repository.
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"costlocker": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "mcp-costlocker"],
|
|
20
|
+
"env": {
|
|
21
|
+
"COSTLOCKER_API_KEY": "YOUR_STATIC_API_KEY"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Optional: pin a version with `mcp-costlocker@0.1.0` instead of trusting `latest` resolution via `npx`.
|
|
29
|
+
|
|
30
|
+
The name `costlocker-mcp` is already taken on npm; this package publishes as `mcp-costlocker`. Before the first publish, confirm the name is still free: `npm view mcp-costlocker`.
|
|
31
|
+
|
|
32
|
+
### From a local clone (npm, yarn, or pnpm)
|
|
33
|
+
|
|
34
|
+
Install dependencies once in the repo, then point MCP at `npm` (or your preferred package manager) and the `start` script. Example for npm:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"costlocker": {
|
|
40
|
+
"command": "npm",
|
|
41
|
+
"args": ["start"],
|
|
42
|
+
"cwd": "/absolute/path/to/costlocker-mcp",
|
|
43
|
+
"env": {
|
|
44
|
+
"COSTLOCKER_API_KEY": "YOUR_STATIC_API_KEY"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
|
|
53
|
+
- Replace `cwd` with the absolute path to this repository.
|
|
54
|
+
- `pnpm start` or `yarn start` work the same way if you prefer those tools.
|
|
55
|
+
|
|
56
|
+
Development builds TypeScript on the fly (`tsx src/index.ts`). For a run that uses the compiled server only, run `npm run build` and start with `node dist/index.js` (or rely on the published package, which already uses `dist/`).
|
|
57
|
+
|
|
58
|
+
### Integrating with Cursor
|
|
59
|
+
|
|
60
|
+
Use one of the JSON snippets above in Cursor MCP settings (`npx` for published package, or `npm` + `cwd` for a local clone).
|
|
61
|
+
|
|
62
|
+
### Integrating with Antigravity
|
|
63
|
+
|
|
64
|
+
Antigravity can connect natively to MCP servers via its configuration file.
|
|
65
|
+
|
|
66
|
+
1. Locate your Antigravity MCP settings.
|
|
67
|
+
2. Add the same `costlocker` server entry as in the Cursor examples (`npx` or local `npm` + `cwd`).
|
|
68
|
+
|
|
69
|
+
### Publishing (maintainers)
|
|
70
|
+
|
|
71
|
+
- `npm run build` produces `dist/` (also run automatically via `prepublishOnly` before `npm publish`).
|
|
72
|
+
- Unscoped packages are public on npm; you only need a logged-in account (`npm login`) and an unused package name. Publish with `npm publish` (no `--access public` required unless you use a scoped name).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { getCostlockerEntriesList, getCostlockerEntriesListForPerson, } from "./integrations/costlocker/queries/getCostlockerEntries.js";
|
|
7
|
+
import { updateCostlockerEntryDescriptions, } from "./integrations/costlocker/mutations/updateCostlockerEntry.js";
|
|
8
|
+
import { sdk } from "./integrations/costlocker/client.js";
|
|
9
|
+
import { registerCostlockerGraphTools } from "./tools/registerCostlockerGraphTools.js";
|
|
10
|
+
// --- Env validation ---
|
|
11
|
+
const COSTLOCKER_API_KEY = process.env.COSTLOCKER_API_KEY;
|
|
12
|
+
if (!COSTLOCKER_API_KEY) {
|
|
13
|
+
process.stderr.write("Missing COSTLOCKER_API_KEY environment variable.\n");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
// --- Server setup ---
|
|
17
|
+
const server = new McpServer({ name: "mcp-costlocker", version: "1.0.0" });
|
|
18
|
+
server.registerTool("costlocker_get_time_entries", {
|
|
19
|
+
description: "Get Costlocker time entries for a given date. Optionally filter by person ID.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
target_date: z.string().describe("Date in YYYY-MM-DD format"),
|
|
22
|
+
person_id: z.number().optional().describe("Optional person ID to filter by. If omitted, returns entries for all people."),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ target_date, person_id }) => {
|
|
25
|
+
try {
|
|
26
|
+
const hasPersonId = typeof person_id !== "undefined";
|
|
27
|
+
const entries = hasPersonId
|
|
28
|
+
? await getCostlockerEntriesListForPerson({
|
|
29
|
+
target_date,
|
|
30
|
+
person_id,
|
|
31
|
+
})
|
|
32
|
+
: await getCostlockerEntriesList({ target_date });
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(entries, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
server.registerTool("costlocker_update_time_entries", {
|
|
46
|
+
description: "Update descriptions of Costlocker time entries given their UUIDs.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
updates: z.array(z.object({
|
|
49
|
+
uuid: z.string(),
|
|
50
|
+
description: z.string(),
|
|
51
|
+
})).min(1).describe("List of entries to update containing the UUID and the new description."),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ updates }) => {
|
|
54
|
+
try {
|
|
55
|
+
const results = await updateCostlockerEntryDescriptions({ updates });
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
registerCostlockerGraphTools(server, sdk);
|
|
69
|
+
// --- Error handling ---
|
|
70
|
+
process.on("uncaughtException", (error) => {
|
|
71
|
+
process.stderr.write(`Uncaught Exception: ${error}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
process.on("unhandledRejection", (error) => {
|
|
75
|
+
process.stderr.write(`Unhandled Rejection: ${error}\n`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
78
|
+
// --- Start ---
|
|
79
|
+
async function main() {
|
|
80
|
+
const transport = new StdioServerTransport();
|
|
81
|
+
await server.connect(transport);
|
|
82
|
+
process.stderr.write("Costlocker MCP server started dynamically\n");
|
|
83
|
+
}
|
|
84
|
+
main().catch((error) => {
|
|
85
|
+
process.stderr.write(`Startup Error: ${error}\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { getSdk } from "./generated/client.js";
|
|
3
|
+
export function validateDate(date) {
|
|
4
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
5
|
+
throw new Error(`Invalid date "${date}". Expected YYYY-MM-DD format.`);
|
|
6
|
+
}
|
|
7
|
+
return date;
|
|
8
|
+
}
|
|
9
|
+
export function toNextDate(date) {
|
|
10
|
+
const dateObj = new Date(`${date}T00:00:00.000Z`);
|
|
11
|
+
if (Number.isNaN(dateObj.getTime())) {
|
|
12
|
+
throw new Error(`Invalid date "${date}"`);
|
|
13
|
+
}
|
|
14
|
+
dateObj.setUTCDate(dateObj.getUTCDate() + 1);
|
|
15
|
+
return dateObj.toISOString().slice(0, 10);
|
|
16
|
+
}
|
|
17
|
+
function requireEnv(name) {
|
|
18
|
+
const value = process.env[name];
|
|
19
|
+
if (!value) {
|
|
20
|
+
throw new Error(`Missing ${name} in environment.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function getEndpoint() {
|
|
25
|
+
return process.env.COSTLOCKER_GRAPHQL_URL ?? "https://api.costlocker.com/graphql";
|
|
26
|
+
}
|
|
27
|
+
function getHeaders() {
|
|
28
|
+
return {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Accept: "application/json",
|
|
31
|
+
Authorization: `Static ${requireEnv("COSTLOCKER_API_KEY")}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function postGraphQl(query, variables) {
|
|
35
|
+
const response = await fetch(getEndpoint(), {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: getHeaders(),
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
query,
|
|
40
|
+
variables,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const bodyText = typeof response.text === "function" ? await response.text() : "";
|
|
45
|
+
throw new Error(`Costlocker request failed with ${response.status} ${response.statusText}${bodyText ? "\\n" + bodyText : ""}`);
|
|
46
|
+
}
|
|
47
|
+
const payload = (await response.json());
|
|
48
|
+
if (payload.errors?.length) {
|
|
49
|
+
throw new Error(`GraphQL error: ${payload.errors.map((error) => error.message).join("; ")}`);
|
|
50
|
+
}
|
|
51
|
+
return payload;
|
|
52
|
+
}
|
|
53
|
+
const requester = async (doc, vars) => {
|
|
54
|
+
const queryString = typeof doc === "string" ? doc : doc?.loc?.source?.body ?? String(doc);
|
|
55
|
+
const payload = await postGraphQl(queryString, vars || {});
|
|
56
|
+
return payload.data;
|
|
57
|
+
};
|
|
58
|
+
export const sdk = getSdk(requester);
|