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 +175 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/pim-client.d.ts +13 -0
- package/dist/pim-client.js +58 -0
- package/dist/tools/calendar.d.ts +3 -0
- package/dist/tools/calendar.js +45 -0
- package/dist/tools/email.d.ts +3 -0
- package/dist/tools/email.js +58 -0
- package/dist/tools/health.d.ts +3 -0
- package/dist/tools/health.js +16 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.js +39 -0
- package/package.json +52 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
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,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,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,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,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
|
+
}
|