pim-mcp-server 1.0.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 ADDED
@@ -0,0 +1,175 @@
1
+ # PIM MCP Server
2
+
3
+ MCP server (stdio transport) that wraps the PIM Gateway REST API into typed MCP tools. Provides email, calendar, and task management capabilities to AI agents.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 20+
8
+ - PIM Gateway running behind Kong
9
+ - Authentik OAuth2 credentials
10
+
11
+ ## Installation
12
+
13
+ ### From npm
14
+
15
+ ```bash
16
+ npm install -g pim-mcp-server
17
+ ```
18
+
19
+ Or run directly with npx:
20
+
21
+ ```bash
22
+ npx pim-mcp-server
23
+ ```
24
+
25
+ ### From source
26
+
27
+ ```bash
28
+ cd pim-mcp-server
29
+ npm install
30
+ npm run build
31
+ ```
32
+
33
+ ## Environment Variables
34
+
35
+ | Variable | Required | Default | Description |
36
+ |---|---|---|---|
37
+ | `PIM_AUTHENTIK_URL` | Yes | — | Authentik URL (e.g. `https://argus:9443`) |
38
+ | `PIM_KONG_URL` | Yes | — | Kong proxy URL (e.g. `https://argus:8443`) |
39
+ | `PIM_CLIENT_ID` | No | `pim-gateway` | OAuth2 client ID |
40
+ | `PIM_CLIENT_SECRET` | Yes | — | OAuth2 client secret |
41
+
42
+ ## Available Tools (24)
43
+
44
+ ### Email (9)
45
+ | Tool | Description |
46
+ |---|---|
47
+ | `pim_list_emails` | List emails from a Gmail label |
48
+ | `pim_search_emails` | Search emails using Gmail query syntax |
49
+ | `pim_get_email` | Get full email content |
50
+ | `pim_send_email` | Send a new email |
51
+ | `pim_reply_email` | Reply to an email |
52
+ | `pim_list_attachments` | List email attachments |
53
+ | `pim_download_attachment` | Download an attachment |
54
+ | `pim_trash_email` | Trash an email (admin) |
55
+ | `pim_delete_email` | Permanently delete (admin) |
56
+
57
+ ### Calendar (5)
58
+ | Tool | Description |
59
+ |---|---|
60
+ | `pim_list_events` | List events in a time range |
61
+ | `pim_get_event` | Get event details |
62
+ | `pim_create_event` | Create a new event |
63
+ | `pim_update_event` | Update an event |
64
+ | `pim_delete_event` | Delete an event (admin) |
65
+
66
+ ### Tasks (7)
67
+ | Tool | Description |
68
+ |---|---|
69
+ | `pim_list_task_lists` | List all task lists |
70
+ | `pim_list_tasks` | List tasks in a list |
71
+ | `pim_get_task` | Get task details |
72
+ | `pim_create_task` | Create a new task |
73
+ | `pim_update_task` | Update a task |
74
+ | `pim_complete_task` | Mark task complete |
75
+ | `pim_delete_task` | Delete a task (admin) |
76
+
77
+ ### Health & Admin (3)
78
+ | Tool | Description |
79
+ |---|---|
80
+ | `pim_health` | Liveness check |
81
+ | `pim_health_ready` | Readiness check (DB + Google API) |
82
+ | `pim_audit_log` | Query audit log (admin) |
83
+
84
+ ## Client Configuration
85
+
86
+ ### Claude Code
87
+
88
+ Add to `.claude/settings.json` or project `settings.json`:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "pim": {
94
+ "command": "npx",
95
+ "args": ["pim-mcp-server"],
96
+ "env": {
97
+ "PIM_AUTHENTIK_URL": "https://argus:9443",
98
+ "PIM_KONG_URL": "https://argus:8443",
99
+ "PIM_CLIENT_SECRET": "<your-secret>"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Claude Desktop
107
+
108
+ Add to `claude_desktop_config.json`:
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "pim": {
114
+ "command": "npx",
115
+ "args": ["pim-mcp-server"],
116
+ "env": {
117
+ "PIM_AUTHENTIK_URL": "https://argus:9443",
118
+ "PIM_KONG_URL": "https://argus:8443",
119
+ "PIM_CLIENT_SECRET": "<your-secret>"
120
+ }
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### OpenClaw (on Jarvis)
127
+
128
+ ```bash
129
+ export PIM_AUTHENTIK_URL=https://argus:9443
130
+ export PIM_KONG_URL=https://argus:8443
131
+ export PIM_CLIENT_SECRET=<your-secret>
132
+ npx pim-mcp-server
133
+ ```
134
+
135
+ ## Testing
136
+
137
+ Run the smoke test to verify connectivity:
138
+
139
+ ```bash
140
+ PIM_AUTHENTIK_URL=https://argus:9443 \
141
+ PIM_KONG_URL=https://argus:8443 \
142
+ PIM_CLIENT_SECRET=<your-secret> \
143
+ npm test
144
+ ```
145
+
146
+ ## Building & Publishing
147
+
148
+ ### Build
149
+
150
+ ```bash
151
+ npm run build # compiles TypeScript to dist/
152
+ ```
153
+
154
+ ### Publish to npm
155
+
156
+ Requires npm login with 2FA:
157
+
158
+ ```bash
159
+ npm login
160
+ npm publish --otp=<2fa-code>
161
+ ```
162
+
163
+ To publish a new version:
164
+
165
+ ```bash
166
+ npm version patch # or minor, major
167
+ npm publish --otp=<2fa-code>
168
+ ```
169
+
170
+ ## Design
171
+
172
+ - **Role-agnostic:** All endpoints are exposed. RBAC is enforced by the PIM Gateway, not this server. The same binary serves OpenClaw (agent role) and Claude Desktop (admin role) — only the credentials differ.
173
+ - **Auth caching:** JWT tokens are cached in memory and auto-refreshed 30 seconds before expiry. On 401, the token is force-refreshed and the request retried once.
174
+ - **Full envelope:** Tool responses return the complete PIM Gateway response envelope (`status`, `data`, `error`, `meta`) for debugging and traceability.
175
+ - **Logging:** Token refreshes and API calls (method, path, status, duration) are logged to stderr.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export declare class TokenManager {
2
+ private authentikUrl;
3
+ private clientId;
4
+ private clientSecret;
5
+ private token;
6
+ private expiresAt;
7
+ private refreshing;
8
+ constructor(authentikUrl: string, clientId: string, clientSecret: string);
9
+ getToken(): Promise<string>;
10
+ forceRefresh(): Promise<string>;
11
+ private refresh;
12
+ private fetchToken;
13
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,62 @@
1
+ // Skip TLS verification for self-signed certs
2
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
3
+ export class TokenManager {
4
+ authentikUrl;
5
+ clientId;
6
+ clientSecret;
7
+ token = null;
8
+ expiresAt = 0;
9
+ refreshing = null;
10
+ constructor(authentikUrl, clientId, clientSecret) {
11
+ this.authentikUrl = authentikUrl;
12
+ this.clientId = clientId;
13
+ this.clientSecret = clientSecret;
14
+ }
15
+ async getToken() {
16
+ if (this.token && Date.now() / 1000 < this.expiresAt - 30) {
17
+ return this.token;
18
+ }
19
+ return this.refresh();
20
+ }
21
+ async forceRefresh() {
22
+ this.token = null;
23
+ this.expiresAt = 0;
24
+ return this.refresh();
25
+ }
26
+ async refresh() {
27
+ if (this.refreshing)
28
+ return this.refreshing;
29
+ this.refreshing = this.fetchToken();
30
+ try {
31
+ return await this.refreshing;
32
+ }
33
+ finally {
34
+ this.refreshing = null;
35
+ }
36
+ }
37
+ async fetchToken() {
38
+ const url = `${this.authentikUrl}/application/o/token/`;
39
+ const body = new URLSearchParams({
40
+ grant_type: "client_credentials",
41
+ client_id: this.clientId,
42
+ client_secret: this.clientSecret,
43
+ scope: "openid pim-role",
44
+ });
45
+ const start = Date.now();
46
+ const resp = await fetch(url, {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
49
+ body: body.toString(),
50
+ });
51
+ if (!resp.ok) {
52
+ const text = await resp.text();
53
+ throw new Error(`Token request failed (${resp.status}): ${text}`);
54
+ }
55
+ const data = (await resp.json());
56
+ this.token = data.access_token;
57
+ this.expiresAt = Date.now() / 1000 + data.expires_in;
58
+ const duration = Date.now() - start;
59
+ console.error(`[auth] Token refreshed (expires in ${data.expires_in}s, took ${duration}ms)`);
60
+ return this.token;
61
+ }
62
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { TokenManager } from "./auth.js";
5
+ import { PIMClient } from "./pim-client.js";
6
+ import { registerEmailTools } from "./tools/email.js";
7
+ import { registerCalendarTools } from "./tools/calendar.js";
8
+ import { registerTasksTools } from "./tools/tasks.js";
9
+ import { registerHealthTools } from "./tools/health.js";
10
+ function requiredEnv(name) {
11
+ const value = process.env[name];
12
+ if (!value) {
13
+ console.error(`[pim-mcp] ERROR: ${name} environment variable is required`);
14
+ process.exit(1);
15
+ }
16
+ return value;
17
+ }
18
+ const authentikUrl = requiredEnv("PIM_AUTHENTIK_URL");
19
+ const kongUrl = requiredEnv("PIM_KONG_URL");
20
+ const clientId = process.env.PIM_CLIENT_ID ?? "pim-gateway";
21
+ const clientSecret = requiredEnv("PIM_CLIENT_SECRET");
22
+ const tokenManager = new TokenManager(authentikUrl, clientId, clientSecret);
23
+ const pimClient = new PIMClient(kongUrl, tokenManager);
24
+ const server = new McpServer({
25
+ name: "pim-gateway",
26
+ version: "1.0.0",
27
+ });
28
+ registerEmailTools(server, pimClient);
29
+ registerCalendarTools(server, pimClient);
30
+ registerTasksTools(server, pimClient);
31
+ registerHealthTools(server, pimClient);
32
+ async function main() {
33
+ const transport = new StdioServerTransport();
34
+ await server.connect(transport);
35
+ console.error("[pim-mcp] Server started (stdio transport)");
36
+ }
37
+ main().catch((err) => {
38
+ console.error("[pim-mcp] Fatal error:", err);
39
+ process.exit(1);
40
+ });
@@ -0,0 +1,13 @@
1
+ import type { TokenManager } from "./auth.js";
2
+ interface RequestOptions {
3
+ params?: Record<string, string | number | boolean | undefined>;
4
+ json?: Record<string, unknown>;
5
+ }
6
+ export declare class PIMClient {
7
+ private kongUrl;
8
+ private tokenManager;
9
+ constructor(kongUrl: string, tokenManager: TokenManager);
10
+ request(method: string, path: string, opts?: RequestOptions): Promise<Record<string, unknown>>;
11
+ private doRequest;
12
+ }
13
+ export {};
@@ -0,0 +1,58 @@
1
+ export class PIMClient {
2
+ kongUrl;
3
+ tokenManager;
4
+ constructor(kongUrl, tokenManager) {
5
+ this.kongUrl = kongUrl;
6
+ this.tokenManager = tokenManager;
7
+ }
8
+ async request(method, path, opts) {
9
+ const result = await this.doRequest(method, path, opts);
10
+ if (result.status === 401) {
11
+ console.error(`[pim] 401 on ${method} ${path}, refreshing token...`);
12
+ await this.tokenManager.forceRefresh();
13
+ return this.doRequest(method, path, opts);
14
+ }
15
+ return result;
16
+ }
17
+ async doRequest(method, path, opts) {
18
+ const token = await this.tokenManager.getToken();
19
+ let url = `${this.kongUrl}/api/v1/pim${path}`;
20
+ if (opts?.params) {
21
+ const searchParams = new URLSearchParams();
22
+ for (const [key, value] of Object.entries(opts.params)) {
23
+ if (value !== undefined && value !== null) {
24
+ searchParams.set(key, String(value));
25
+ }
26
+ }
27
+ const qs = searchParams.toString();
28
+ if (qs)
29
+ url += `?${qs}`;
30
+ }
31
+ const headers = {
32
+ Authorization: `Bearer ${token}`,
33
+ };
34
+ const fetchOpts = {
35
+ method,
36
+ headers,
37
+ };
38
+ if (opts?.json) {
39
+ headers["Content-Type"] = "application/json";
40
+ fetchOpts.body = JSON.stringify(opts.json);
41
+ }
42
+ const start = Date.now();
43
+ const resp = await fetch(url, fetchOpts);
44
+ const duration = Date.now() - start;
45
+ let body;
46
+ try {
47
+ body = (await resp.json());
48
+ }
49
+ catch {
50
+ body = { status: "error", error: { message: await resp.text() } };
51
+ }
52
+ console.error(`[pim] ${method} ${path} → ${resp.status} (${duration}ms)`);
53
+ if (!resp.ok && resp.status === 401) {
54
+ return { status: 401 };
55
+ }
56
+ return body;
57
+ }
58
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PIMClient } from "../pim-client.js";
3
+ export declare function registerCalendarTools(server: McpServer, client: PIMClient): void;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ function textResult(data) {
3
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
4
+ }
5
+ export function registerCalendarTools(server, client) {
6
+ server.tool("pim_list_events", "List calendar events within a time range", {
7
+ time_min: z.string().optional().describe("Start of range (ISO8601, defaults to now)"),
8
+ time_max: z.string().optional().describe("End of range (ISO8601, defaults to +7 days)"),
9
+ max_results: z.number().default(50).describe("Number of results (1-250)"),
10
+ calendar_id: z.string().default("primary").describe("Calendar to query"),
11
+ q: z.string().optional().describe("Free-text search query"),
12
+ }, async (params) => textResult(await client.request("GET", "/calendar/events", { params })));
13
+ server.tool("pim_get_event", "Get details of a specific calendar event", {
14
+ event_id: z.string().describe("Event ID"),
15
+ calendar_id: z.string().default("primary").describe("Calendar to query"),
16
+ }, async ({ event_id, calendar_id }) => textResult(await client.request("GET", `/calendar/events/${event_id}`, {
17
+ params: { calendar_id },
18
+ })));
19
+ server.tool("pim_create_event", "Create a new calendar event", {
20
+ summary: z.string().describe("Event title"),
21
+ start: z.string().describe("Start time (ISO8601 with timezone)"),
22
+ end: z.string().describe("End time (ISO8601 with timezone)"),
23
+ description: z.string().optional().describe("Event description"),
24
+ location: z.string().optional().describe("Event location"),
25
+ attendees: z.array(z.string()).optional().describe("Attendee email addresses"),
26
+ }, async (params) => textResult(await client.request("POST", "/calendar/events", { json: params })));
27
+ server.tool("pim_update_event", "Update a calendar event (only include fields to change)", {
28
+ event_id: z.string().describe("Event ID"),
29
+ calendar_id: z.string().default("primary").describe("Calendar"),
30
+ summary: z.string().optional().describe("New title"),
31
+ start: z.string().optional().describe("New start time (ISO8601)"),
32
+ end: z.string().optional().describe("New end time (ISO8601)"),
33
+ description: z.string().optional().describe("New description"),
34
+ location: z.string().optional().describe("New location"),
35
+ }, async ({ event_id, calendar_id, ...updates }) => textResult(await client.request("PUT", `/calendar/events/${event_id}`, {
36
+ params: { calendar_id },
37
+ json: updates,
38
+ })));
39
+ server.tool("pim_delete_event", "Delete a calendar event (requires admin role)", {
40
+ event_id: z.string().describe("Event ID"),
41
+ calendar_id: z.string().default("primary").describe("Calendar"),
42
+ }, async ({ event_id, calendar_id }) => textResult(await client.request("DELETE", `/calendar/events/${event_id}`, {
43
+ params: { calendar_id },
44
+ })));
45
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PIMClient } from "../pim-client.js";
3
+ export declare function registerEmailTools(server: McpServer, client: PIMClient): void;
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ function textResult(data) {
3
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
4
+ }
5
+ export function registerEmailTools(server, client) {
6
+ server.tool("pim_list_emails", "List emails from a Gmail label", {
7
+ label: z.string().default("INBOX").describe("Gmail label (INBOX, SENT, etc.)"),
8
+ max_results: z.number().default(20).describe("Number of results (1-100)"),
9
+ page_token: z.string().optional().describe("Pagination token from previous response"),
10
+ }, async (params) => textResult(await client.request("GET", "/email/messages", { params })));
11
+ server.tool("pim_search_emails", "Search emails using Gmail query syntax (e.g. 'from:alice subject:meeting', 'is:unread')", {
12
+ q: z.string().describe("Gmail search query"),
13
+ max_results: z.number().default(20).describe("Number of results (1-100)"),
14
+ page_token: z.string().optional().describe("Pagination token"),
15
+ }, async (params) => textResult(await client.request("GET", "/email/messages/search", { params })));
16
+ server.tool("pim_get_email", "Get full email content including body, cc, bcc, and attachments info", {
17
+ message_id: z.string().describe("Gmail message ID"),
18
+ }, async ({ message_id }) => textResult(await client.request("GET", `/email/messages/${message_id}`)));
19
+ server.tool("pim_send_email", "Send a new email", {
20
+ to: z.array(z.string()).describe("Recipient email addresses"),
21
+ subject: z.string().describe("Email subject"),
22
+ body: z.string().describe("Email body text"),
23
+ body_type: z.enum(["plain", "html"]).default("plain").describe("Body format"),
24
+ cc: z.array(z.string()).optional().describe("CC recipients"),
25
+ bcc: z.array(z.string()).optional().describe("BCC recipients"),
26
+ }, async (params) => textResult(await client.request("POST", "/email/messages/send", {
27
+ json: {
28
+ to: params.to,
29
+ subject: params.subject,
30
+ body: params.body,
31
+ body_type: params.body_type,
32
+ cc: params.cc ?? [],
33
+ bcc: params.bcc ?? [],
34
+ },
35
+ })));
36
+ server.tool("pim_reply_email", "Reply to an email (auto-sets In-Reply-To and thread headers)", {
37
+ message_id: z.string().describe("Message ID to reply to"),
38
+ body: z.string().describe("Reply body text"),
39
+ body_type: z.enum(["plain", "html"]).default("plain").describe("Body format"),
40
+ cc: z.array(z.string()).optional().describe("CC recipients"),
41
+ bcc: z.array(z.string()).optional().describe("BCC recipients"),
42
+ }, async ({ message_id, ...rest }) => textResult(await client.request("POST", `/email/messages/${message_id}/reply`, {
43
+ json: { body: rest.body, body_type: rest.body_type, cc: rest.cc ?? [], bcc: rest.bcc ?? [] },
44
+ })));
45
+ server.tool("pim_list_attachments", "List attachments for an email (returns id, filename, mime_type, size)", {
46
+ message_id: z.string().describe("Gmail message ID"),
47
+ }, async ({ message_id }) => textResult(await client.request("GET", `/email/messages/${message_id}/attachments`)));
48
+ server.tool("pim_download_attachment", "Download an email attachment (returns metadata; binary content not supported via MCP)", {
49
+ message_id: z.string().describe("Gmail message ID"),
50
+ attachment_id: z.string().describe("Attachment ID"),
51
+ }, async ({ message_id, attachment_id }) => textResult(await client.request("GET", `/email/messages/${message_id}/attachments/${attachment_id}`)));
52
+ server.tool("pim_trash_email", "Move an email to trash (requires admin role)", {
53
+ message_id: z.string().describe("Gmail message ID"),
54
+ }, async ({ message_id }) => textResult(await client.request("DELETE", `/email/messages/${message_id}`)));
55
+ server.tool("pim_delete_email", "Permanently delete an email (requires admin role, irreversible)", {
56
+ message_id: z.string().describe("Gmail message ID"),
57
+ }, async ({ message_id }) => textResult(await client.request("DELETE", `/email/messages/${message_id}/permanent`)));
58
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PIMClient } from "../pim-client.js";
3
+ export declare function registerHealthTools(server: McpServer, client: PIMClient): void;
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ function textResult(data) {
3
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
4
+ }
5
+ export function registerHealthTools(server, client) {
6
+ server.tool("pim_health", "Check PIM Gateway liveness", {}, async () => textResult(await client.request("GET", "/health")));
7
+ server.tool("pim_health_ready", "Check PIM Gateway readiness (database + Google API connectivity)", {}, async () => textResult(await client.request("GET", "/health/ready")));
8
+ server.tool("pim_audit_log", "Query the audit log (requires admin role)", {
9
+ since: z.string().optional().describe("Start time (ISO8601, defaults to -24h)"),
10
+ until: z.string().optional().describe("End time (ISO8601, defaults to now)"),
11
+ subject: z.string().optional().describe("Filter by JWT subject"),
12
+ path: z.string().optional().describe("Filter by request path prefix"),
13
+ status_code: z.number().optional().describe("Filter by HTTP status code"),
14
+ limit: z.number().default(100).describe("Max results (1-1000)"),
15
+ }, async (params) => textResult(await client.request("GET", "/admin/audit", { params })));
16
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { PIMClient } from "../pim-client.js";
3
+ export declare function registerTasksTools(server: McpServer, client: PIMClient): void;
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ function textResult(data) {
3
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
4
+ }
5
+ export function registerTasksTools(server, client) {
6
+ server.tool("pim_list_task_lists", "List all task lists", {}, async () => textResult(await client.request("GET", "/tasks/lists")));
7
+ server.tool("pim_list_tasks", "List tasks in a task list", {
8
+ list_id: z.string().describe("Task list ID"),
9
+ show_completed: z.boolean().default(false).describe("Include completed tasks"),
10
+ max_results: z.number().default(50).describe("Number of results (1-100)"),
11
+ page_token: z.string().optional().describe("Pagination token"),
12
+ }, async ({ list_id, ...params }) => textResult(await client.request("GET", `/tasks/lists/${list_id}/tasks`, { params })));
13
+ server.tool("pim_get_task", "Get details of a specific task", {
14
+ list_id: z.string().describe("Task list ID"),
15
+ task_id: z.string().describe("Task ID"),
16
+ }, async ({ list_id, task_id }) => textResult(await client.request("GET", `/tasks/lists/${list_id}/tasks/${task_id}`)));
17
+ server.tool("pim_create_task", "Create a new task", {
18
+ list_id: z.string().describe("Task list ID"),
19
+ title: z.string().describe("Task title"),
20
+ notes: z.string().optional().describe("Task notes/description"),
21
+ due: z.string().optional().describe("Due date (ISO8601 date: YYYY-MM-DD)"),
22
+ }, async ({ list_id, ...body }) => textResult(await client.request("POST", `/tasks/lists/${list_id}/tasks`, { json: body })));
23
+ server.tool("pim_update_task", "Update a task (only include fields to change)", {
24
+ list_id: z.string().describe("Task list ID"),
25
+ task_id: z.string().describe("Task ID"),
26
+ title: z.string().optional().describe("New title"),
27
+ notes: z.string().optional().describe("New notes"),
28
+ due: z.string().optional().describe("New due date (ISO8601)"),
29
+ status: z.string().optional().describe("New status"),
30
+ }, async ({ list_id, task_id, ...updates }) => textResult(await client.request("PUT", `/tasks/lists/${list_id}/tasks/${task_id}`, { json: updates })));
31
+ server.tool("pim_complete_task", "Mark a task as complete", {
32
+ list_id: z.string().describe("Task list ID"),
33
+ task_id: z.string().describe("Task ID"),
34
+ }, async ({ list_id, task_id }) => textResult(await client.request("PATCH", `/tasks/lists/${list_id}/tasks/${task_id}/complete`)));
35
+ server.tool("pim_delete_task", "Delete a task (requires admin role)", {
36
+ list_id: z.string().describe("Task list ID"),
37
+ task_id: z.string().describe("Task ID"),
38
+ }, async ({ list_id, task_id }) => textResult(await client.request("DELETE", `/tasks/lists/${list_id}/tasks/${task_id}`)));
39
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "pim-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for PIM Gateway — email, calendar, and tasks management via typed MCP tools",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "pim-mcp-server": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepublishOnly": "npm run build",
17
+ "start": "node dist/index.js",
18
+ "dev": "tsx src/index.ts",
19
+ "test": "tsx scripts/test.ts"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "pim",
25
+ "email",
26
+ "calendar",
27
+ "tasks",
28
+ "gmail",
29
+ "google-calendar",
30
+ "google-tasks",
31
+ "ai-agent"
32
+ ],
33
+ "author": "Michael Tan",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/michaeltansg/pim-gateway.git",
38
+ "directory": "pim-mcp-server"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.27.0",
45
+ "zod": "^3.25.0"
46
+ },
47
+ "devDependencies": {
48
+ "typescript": "^5.8.0",
49
+ "tsx": "^4.19.0",
50
+ "@types/node": "^22.0.0"
51
+ }
52
+ }