gmail-workspace-mcp-server 0.0.3
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 +141 -0
- package/build/index.d.ts +2 -0
- package/build/index.integration-with-mock.d.ts +2 -0
- package/build/index.integration-with-mock.js +106 -0
- package/build/index.js +58 -0
- package/package.json +48 -0
- package/shared/gmail-client/lib/api-errors.d.ts +7 -0
- package/shared/gmail-client/lib/api-errors.js +21 -0
- package/shared/gmail-client/lib/get-message.d.ts +9 -0
- package/shared/gmail-client/lib/get-message.js +24 -0
- package/shared/gmail-client/lib/list-messages.d.ts +15 -0
- package/shared/gmail-client/lib/list-messages.js +35 -0
- package/shared/index.d.ts +4 -0
- package/shared/index.js +7 -0
- package/shared/logging.d.ts +10 -0
- package/shared/logging.js +21 -0
- package/shared/server.d.ts +117 -0
- package/shared/server.js +117 -0
- package/shared/tools/get-email.d.ts +37 -0
- package/shared/tools/get-email.js +192 -0
- package/shared/tools/list-recent-emails.d.ts +54 -0
- package/shared/tools/list-recent-emails.js +133 -0
- package/shared/tools.d.ts +10 -0
- package/shared/tools.js +45 -0
- package/shared/types.d.ts +66 -0
- package/shared/types.js +5 -0
- package/shared/utils/email-helpers.d.ts +5 -0
- package/shared/utils/email-helpers.js +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Gmail Workspace MCP Server
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that provides Gmail integration for AI assistants using Google Workspace service accounts with domain-wide delegation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **List Recent Emails**: Retrieve recent emails within a specified time horizon
|
|
8
|
+
- **Get Email Details**: Fetch full email content including body and attachments info
|
|
9
|
+
- **Service Account Authentication**: Secure domain-wide delegation for Google Workspace organizations
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install gmail-workspace-mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run directly with npx:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx gmail-workspace-mcp-server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
This server requires a Google Cloud service account with domain-wide delegation to access Gmail on behalf of users in your Google Workspace domain.
|
|
26
|
+
|
|
27
|
+
### Setup Steps
|
|
28
|
+
|
|
29
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
30
|
+
2. Create or select a project
|
|
31
|
+
3. Enable the Gmail API
|
|
32
|
+
4. Create a service account with domain-wide delegation enabled
|
|
33
|
+
5. In [Google Workspace Admin Console](https://admin.google.com/), grant the service account access to the `https://www.googleapis.com/auth/gmail.readonly` scope
|
|
34
|
+
6. Download the JSON key file
|
|
35
|
+
|
|
36
|
+
### Environment Variables
|
|
37
|
+
|
|
38
|
+
| Variable | Required | Description |
|
|
39
|
+
| ------------------------------------ | -------- | ---------------------------------------- |
|
|
40
|
+
| `GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL` | Yes | Service account email address |
|
|
41
|
+
| `GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY` | Yes | Service account private key (PEM format) |
|
|
42
|
+
| `GMAIL_IMPERSONATE_EMAIL` | Yes | Email address to impersonate |
|
|
43
|
+
|
|
44
|
+
You can find the `client_email` and `private_key` values in your service account JSON key file.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
### Claude Desktop
|
|
49
|
+
|
|
50
|
+
Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"gmail": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["gmail-workspace-mcp-server"],
|
|
58
|
+
"env": {
|
|
59
|
+
"GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL": "my-service-account@my-project.iam.gserviceaccount.com",
|
|
60
|
+
"GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY": "-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----",
|
|
61
|
+
"GMAIL_IMPERSONATE_EMAIL": "user@yourdomain.com"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Note:** For the private key, you can either:
|
|
69
|
+
|
|
70
|
+
1. Use the key directly with `\n` for newlines (as shown above)
|
|
71
|
+
2. Set the environment variable from a shell that preserves newlines
|
|
72
|
+
|
|
73
|
+
## Available Tools
|
|
74
|
+
|
|
75
|
+
### gmail_list_recent_emails
|
|
76
|
+
|
|
77
|
+
List recent emails from Gmail within a specified time horizon.
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
|
|
81
|
+
- `hours` (number, optional): Time horizon in hours (default: 24)
|
|
82
|
+
- `labels` (string, optional): Comma-separated label IDs (default: "INBOX")
|
|
83
|
+
- `max_results` (number, optional): Maximum emails to return (default: 10, max: 100)
|
|
84
|
+
|
|
85
|
+
**Example:**
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"hours": 48,
|
|
90
|
+
"labels": "INBOX,STARRED",
|
|
91
|
+
"max_results": 20
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### gmail_get_email
|
|
96
|
+
|
|
97
|
+
Retrieve the full content of a specific email by its ID.
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
|
|
101
|
+
- `email_id` (string, required): The unique identifier of the email
|
|
102
|
+
|
|
103
|
+
**Example:**
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"email_id": "18abc123def456"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
### Setup
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Install dependencies
|
|
117
|
+
npm run install-all
|
|
118
|
+
|
|
119
|
+
# Build
|
|
120
|
+
npm run build
|
|
121
|
+
|
|
122
|
+
# Run in development mode
|
|
123
|
+
npm run dev
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Testing
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Run functional tests
|
|
130
|
+
npm test
|
|
131
|
+
|
|
132
|
+
# Run integration tests
|
|
133
|
+
npm run test:integration
|
|
134
|
+
|
|
135
|
+
# Run manual tests (requires service account credentials)
|
|
136
|
+
npm run test:manual
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Integration test entry point with mock data
|
|
4
|
+
* Used for running integration tests without real Gmail API access
|
|
5
|
+
*/
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { createMCPServer } from '../shared/index.js';
|
|
8
|
+
import { logServerStart, logError } from '../shared/logging.js';
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// MOCK DATA
|
|
11
|
+
// =============================================================================
|
|
12
|
+
const MOCK_EMAILS = [
|
|
13
|
+
{
|
|
14
|
+
id: 'msg_001',
|
|
15
|
+
threadId: 'thread_001',
|
|
16
|
+
labelIds: ['INBOX', 'UNREAD'],
|
|
17
|
+
snippet: 'Hey, just wanted to check in about the project...',
|
|
18
|
+
historyId: '12345',
|
|
19
|
+
internalDate: String(Date.now() - 1000 * 60 * 30), // 30 minutes ago
|
|
20
|
+
payload: {
|
|
21
|
+
mimeType: 'text/plain',
|
|
22
|
+
headers: [
|
|
23
|
+
{ name: 'Subject', value: 'Project Update' },
|
|
24
|
+
{ name: 'From', value: 'alice@example.com' },
|
|
25
|
+
{ name: 'To', value: 'me@example.com' },
|
|
26
|
+
{ name: 'Date', value: new Date(Date.now() - 1000 * 60 * 30).toISOString() },
|
|
27
|
+
],
|
|
28
|
+
body: {
|
|
29
|
+
size: 150,
|
|
30
|
+
data: Buffer.from('Hey, just wanted to check in about the project. How is everything going?').toString('base64url'),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
sizeEstimate: 1024,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'msg_002',
|
|
37
|
+
threadId: 'thread_002',
|
|
38
|
+
labelIds: ['INBOX'],
|
|
39
|
+
snippet: 'Meeting reminder for tomorrow at 2pm...',
|
|
40
|
+
historyId: '12346',
|
|
41
|
+
internalDate: String(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
|
|
42
|
+
payload: {
|
|
43
|
+
mimeType: 'text/plain',
|
|
44
|
+
headers: [
|
|
45
|
+
{ name: 'Subject', value: 'Meeting Reminder' },
|
|
46
|
+
{ name: 'From', value: 'calendar@example.com' },
|
|
47
|
+
{ name: 'To', value: 'me@example.com' },
|
|
48
|
+
{ name: 'Date', value: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString() },
|
|
49
|
+
],
|
|
50
|
+
body: {
|
|
51
|
+
size: 200,
|
|
52
|
+
data: Buffer.from('Meeting reminder for tomorrow at 2pm. Please confirm your attendance.').toString('base64url'),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
sizeEstimate: 2048,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// MOCK CLIENT
|
|
60
|
+
// =============================================================================
|
|
61
|
+
function createMockClient() {
|
|
62
|
+
return {
|
|
63
|
+
async listMessages(options) {
|
|
64
|
+
// Filter by label if specified
|
|
65
|
+
let filtered = MOCK_EMAILS;
|
|
66
|
+
if (options?.labelIds && options.labelIds.length > 0) {
|
|
67
|
+
filtered = MOCK_EMAILS.filter((email) => options.labelIds.some((label) => email.labelIds?.includes(label)));
|
|
68
|
+
}
|
|
69
|
+
// Apply maxResults
|
|
70
|
+
const maxResults = options?.maxResults ?? 10;
|
|
71
|
+
const messages = filtered.slice(0, maxResults).map((e) => ({
|
|
72
|
+
id: e.id,
|
|
73
|
+
threadId: e.threadId,
|
|
74
|
+
}));
|
|
75
|
+
return {
|
|
76
|
+
messages,
|
|
77
|
+
resultSizeEstimate: messages.length,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
async getMessage(messageId, _options) {
|
|
81
|
+
const email = MOCK_EMAILS.find((e) => e.id === messageId);
|
|
82
|
+
if (!email) {
|
|
83
|
+
throw new Error(`Message not found: ${messageId}`);
|
|
84
|
+
}
|
|
85
|
+
return email;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// MAIN ENTRY POINT
|
|
91
|
+
// =============================================================================
|
|
92
|
+
async function main() {
|
|
93
|
+
// Create server with mock client
|
|
94
|
+
const { server, registerHandlers } = createMCPServer();
|
|
95
|
+
// Register handlers with mock client factory
|
|
96
|
+
await registerHandlers(server, createMockClient);
|
|
97
|
+
// Start server with stdio transport
|
|
98
|
+
const transport = new StdioServerTransport();
|
|
99
|
+
await server.connect(transport);
|
|
100
|
+
logServerStart('Gmail (Mock)');
|
|
101
|
+
}
|
|
102
|
+
// Run the server
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
logError('main', error);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
package/build/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { createMCPServer } from '../shared/index.js';
|
|
4
|
+
import { logServerStart, logError } from '../shared/logging.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// ENVIRONMENT VALIDATION
|
|
7
|
+
// =============================================================================
|
|
8
|
+
function validateEnvironment() {
|
|
9
|
+
const missing = [];
|
|
10
|
+
if (!process.env.GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL) {
|
|
11
|
+
missing.push('GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL');
|
|
12
|
+
}
|
|
13
|
+
if (!process.env.GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY) {
|
|
14
|
+
missing.push('GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY');
|
|
15
|
+
}
|
|
16
|
+
if (!process.env.GMAIL_IMPERSONATE_EMAIL) {
|
|
17
|
+
missing.push('GMAIL_IMPERSONATE_EMAIL');
|
|
18
|
+
}
|
|
19
|
+
if (missing.length > 0) {
|
|
20
|
+
logError('validateEnvironment', 'Missing required environment variables:');
|
|
21
|
+
console.error('\nThis MCP server requires a Google Cloud service account with');
|
|
22
|
+
console.error('domain-wide delegation to access Gmail on behalf of users.');
|
|
23
|
+
console.error('\nRequired environment variables:');
|
|
24
|
+
console.error(' GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address');
|
|
25
|
+
console.error(' Example: my-service-account@my-project.iam.gserviceaccount.com');
|
|
26
|
+
console.error(' GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)');
|
|
27
|
+
console.error(' Example: -----BEGIN PRIVATE KEY-----\\nMIIE...\\n-----END PRIVATE KEY-----');
|
|
28
|
+
console.error(' GMAIL_IMPERSONATE_EMAIL: Email address to impersonate');
|
|
29
|
+
console.error(' Example: user@yourdomain.com');
|
|
30
|
+
console.error('\nSetup steps:');
|
|
31
|
+
console.error(' 1. Go to https://console.cloud.google.com/');
|
|
32
|
+
console.error(' 2. Create a service account with domain-wide delegation');
|
|
33
|
+
console.error(' 3. In Google Workspace Admin, grant gmail.readonly scope');
|
|
34
|
+
console.error(' 4. Download the JSON key file and extract client_email and private_key');
|
|
35
|
+
console.error('\n======================================================\n');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// MAIN ENTRY POINT
|
|
41
|
+
// =============================================================================
|
|
42
|
+
async function main() {
|
|
43
|
+
// Step 1: Validate environment variables
|
|
44
|
+
validateEnvironment();
|
|
45
|
+
// Step 2: Create server using factory
|
|
46
|
+
const { server, registerHandlers } = createMCPServer();
|
|
47
|
+
// Step 3: Register all handlers (tools)
|
|
48
|
+
await registerHandlers(server);
|
|
49
|
+
// Step 4: Start server with stdio transport
|
|
50
|
+
const transport = new StdioServerTransport();
|
|
51
|
+
await server.connect(transport);
|
|
52
|
+
logServerStart('Gmail');
|
|
53
|
+
}
|
|
54
|
+
// Run the server
|
|
55
|
+
main().catch((error) => {
|
|
56
|
+
logError('main', error);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gmail-workspace-mcp-server",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "MCP server for Gmail integration with service account support",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gmail-workspace-mcp-server": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build/**/*.js",
|
|
12
|
+
"build/**/*.d.ts",
|
|
13
|
+
"shared/**/*.js",
|
|
14
|
+
"shared/**/*.d.ts",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc && npm run build:integration",
|
|
19
|
+
"build:integration": "tsc -p tsconfig.integration.json",
|
|
20
|
+
"start": "node build/index.js",
|
|
21
|
+
"dev": "tsx src/index.ts",
|
|
22
|
+
"predev": "cd ../shared && npm run build && cd ../local && node setup-dev.js",
|
|
23
|
+
"prebuild": "cd ../shared && npm run build && cd ../local && node setup-dev.js",
|
|
24
|
+
"prepublishOnly": "node prepare-publish.js && node ../scripts/prepare-npm-readme.js",
|
|
25
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
26
|
+
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
27
|
+
"format": "prettier --write .",
|
|
28
|
+
"format:check": "prettier --check .",
|
|
29
|
+
"stage-publish": "npm version"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
33
|
+
"google-auth-library": "^10.5.0",
|
|
34
|
+
"zod": "^3.24.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.10.6",
|
|
38
|
+
"tsx": "^4.19.4",
|
|
39
|
+
"typescript": "^5.7.3"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"gmail",
|
|
44
|
+
"model-context-protocol"
|
|
45
|
+
],
|
|
46
|
+
"author": "PulseMCP",
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles Gmail API errors with structured error messages
|
|
3
|
+
* @param status HTTP status code
|
|
4
|
+
* @param operation Description of the operation that failed
|
|
5
|
+
* @param resourceId Optional resource identifier (e.g., message ID)
|
|
6
|
+
*/
|
|
7
|
+
export declare function handleApiError(status: number, operation: string, resourceId?: string): never;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles Gmail API errors with structured error messages
|
|
3
|
+
* @param status HTTP status code
|
|
4
|
+
* @param operation Description of the operation that failed
|
|
5
|
+
* @param resourceId Optional resource identifier (e.g., message ID)
|
|
6
|
+
*/
|
|
7
|
+
export function handleApiError(status, operation, resourceId) {
|
|
8
|
+
if (status === 401) {
|
|
9
|
+
throw new Error('Service account authentication failed. Verify the key file and domain-wide delegation.');
|
|
10
|
+
}
|
|
11
|
+
if (status === 403) {
|
|
12
|
+
throw new Error('Permission denied. Ensure gmail.readonly scope is granted in Google Workspace Admin.');
|
|
13
|
+
}
|
|
14
|
+
if (status === 429) {
|
|
15
|
+
throw new Error('Gmail API rate limit exceeded. Please try again later.');
|
|
16
|
+
}
|
|
17
|
+
if (status === 404) {
|
|
18
|
+
throw new Error(resourceId ? `Resource not found: ${resourceId}` : 'Gmail resource not found.');
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Gmail API error while ${operation}: HTTP ${status}`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Email } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Gets a specific message by ID
|
|
4
|
+
* Supports different format levels for detail control
|
|
5
|
+
*/
|
|
6
|
+
export declare function getMessage(baseUrl: string, headers: Record<string, string>, messageId: string, options?: {
|
|
7
|
+
format?: 'minimal' | 'full' | 'raw' | 'metadata';
|
|
8
|
+
metadataHeaders?: string[];
|
|
9
|
+
}): Promise<Email>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { handleApiError } from './api-errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Gets a specific message by ID
|
|
4
|
+
* Supports different format levels for detail control
|
|
5
|
+
*/
|
|
6
|
+
export async function getMessage(baseUrl, headers, messageId, options) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
if (options?.format) {
|
|
9
|
+
params.set('format', options.format);
|
|
10
|
+
}
|
|
11
|
+
if (options?.metadataHeaders && options.metadataHeaders.length > 0) {
|
|
12
|
+
options.metadataHeaders.forEach((header) => params.append('metadataHeaders', header));
|
|
13
|
+
}
|
|
14
|
+
const queryString = params.toString();
|
|
15
|
+
const url = `${baseUrl}/messages/${messageId}${queryString ? `?${queryString}` : ''}`;
|
|
16
|
+
const response = await fetch(url, {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
headers,
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
handleApiError(response.status, 'getting message', messageId);
|
|
22
|
+
}
|
|
23
|
+
return (await response.json());
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EmailListItem } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lists messages matching a query
|
|
4
|
+
* Uses pagination to fetch results
|
|
5
|
+
*/
|
|
6
|
+
export declare function listMessages(baseUrl: string, headers: Record<string, string>, options?: {
|
|
7
|
+
q?: string;
|
|
8
|
+
maxResults?: number;
|
|
9
|
+
pageToken?: string;
|
|
10
|
+
labelIds?: string[];
|
|
11
|
+
}): Promise<{
|
|
12
|
+
messages: EmailListItem[];
|
|
13
|
+
nextPageToken?: string;
|
|
14
|
+
resultSizeEstimate?: number;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { handleApiError } from './api-errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lists messages matching a query
|
|
4
|
+
* Uses pagination to fetch results
|
|
5
|
+
*/
|
|
6
|
+
export async function listMessages(baseUrl, headers, options) {
|
|
7
|
+
const params = new URLSearchParams();
|
|
8
|
+
if (options?.q) {
|
|
9
|
+
params.set('q', options.q);
|
|
10
|
+
}
|
|
11
|
+
if (options?.maxResults) {
|
|
12
|
+
params.set('maxResults', options.maxResults.toString());
|
|
13
|
+
}
|
|
14
|
+
if (options?.pageToken) {
|
|
15
|
+
params.set('pageToken', options.pageToken);
|
|
16
|
+
}
|
|
17
|
+
if (options?.labelIds && options.labelIds.length > 0) {
|
|
18
|
+
options.labelIds.forEach((labelId) => params.append('labelIds', labelId));
|
|
19
|
+
}
|
|
20
|
+
const queryString = params.toString();
|
|
21
|
+
const url = `${baseUrl}/messages${queryString ? `?${queryString}` : ''}`;
|
|
22
|
+
const response = await fetch(url, {
|
|
23
|
+
method: 'GET',
|
|
24
|
+
headers,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
handleApiError(response.status, 'listing messages');
|
|
28
|
+
}
|
|
29
|
+
const data = (await response.json());
|
|
30
|
+
return {
|
|
31
|
+
messages: data.messages ?? [],
|
|
32
|
+
nextPageToken: data.nextPageToken,
|
|
33
|
+
resultSizeEstimate: data.resultSizeEstimate,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createMCPServer, createDefaultClient, ServiceAccountGmailClient, type IGmailClient, type ClientFactory, type ServiceAccountCredentials, } from './server.js';
|
|
2
|
+
export { createRegisterTools, registerTools } from './tools.js';
|
|
3
|
+
export { logServerStart, logError, logWarning, logDebug } from './logging.js';
|
|
4
|
+
export type { Email, EmailListItem, EmailHeader, EmailPart, Label, Thread, PaginatedResponse, } from './types.js';
|
package/shared/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Main exports for Gmail MCP Server
|
|
2
|
+
// Server and client
|
|
3
|
+
export { createMCPServer, createDefaultClient, ServiceAccountGmailClient, } from './server.js';
|
|
4
|
+
// Tools
|
|
5
|
+
export { createRegisterTools, registerTools } from './tools.js';
|
|
6
|
+
// Logging utilities
|
|
7
|
+
export { logServerStart, logError, logWarning, logDebug } from './logging.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logging utilities for Gmail MCP Server
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: All logging uses console.error() to write to stderr.
|
|
5
|
+
* The MCP protocol requires stdout to contain only JSON messages.
|
|
6
|
+
*/
|
|
7
|
+
export declare function logServerStart(serverName: string, transport?: string): void;
|
|
8
|
+
export declare function logError(context: string, error: unknown): void;
|
|
9
|
+
export declare function logWarning(context: string, message: string): void;
|
|
10
|
+
export declare function logDebug(context: string, message: string): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logging utilities for Gmail MCP Server
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: All logging uses console.error() to write to stderr.
|
|
5
|
+
* The MCP protocol requires stdout to contain only JSON messages.
|
|
6
|
+
*/
|
|
7
|
+
export function logServerStart(serverName, transport = 'stdio') {
|
|
8
|
+
console.error(`MCP server ${serverName} running on ${transport}`);
|
|
9
|
+
}
|
|
10
|
+
export function logError(context, error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
console.error(`[ERROR] ${context}: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
export function logWarning(context, message) {
|
|
15
|
+
console.error(`[WARN] ${context}: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
export function logDebug(context, message) {
|
|
18
|
+
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
|
19
|
+
console.error(`[DEBUG] ${context}: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { Email, EmailListItem } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Gmail API client interface
|
|
5
|
+
* Defines all methods for interacting with the Gmail API
|
|
6
|
+
*/
|
|
7
|
+
export interface IGmailClient {
|
|
8
|
+
/**
|
|
9
|
+
* List messages matching a query
|
|
10
|
+
*/
|
|
11
|
+
listMessages(options?: {
|
|
12
|
+
q?: string;
|
|
13
|
+
maxResults?: number;
|
|
14
|
+
pageToken?: string;
|
|
15
|
+
labelIds?: string[];
|
|
16
|
+
}): Promise<{
|
|
17
|
+
messages: EmailListItem[];
|
|
18
|
+
nextPageToken?: string;
|
|
19
|
+
resultSizeEstimate?: number;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Get a specific message by ID
|
|
23
|
+
*/
|
|
24
|
+
getMessage(messageId: string, options?: {
|
|
25
|
+
format?: 'minimal' | 'full' | 'raw' | 'metadata';
|
|
26
|
+
metadataHeaders?: string[];
|
|
27
|
+
}): Promise<Email>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Service account credentials structure
|
|
31
|
+
*/
|
|
32
|
+
export interface ServiceAccountCredentials {
|
|
33
|
+
type: string;
|
|
34
|
+
project_id: string;
|
|
35
|
+
private_key_id: string;
|
|
36
|
+
private_key: string;
|
|
37
|
+
client_email: string;
|
|
38
|
+
client_id: string;
|
|
39
|
+
auth_uri: string;
|
|
40
|
+
token_uri: string;
|
|
41
|
+
auth_provider_x509_cert_url: string;
|
|
42
|
+
client_x509_cert_url: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Gmail API client implementation using service account with domain-wide delegation
|
|
46
|
+
*/
|
|
47
|
+
export declare class ServiceAccountGmailClient implements IGmailClient {
|
|
48
|
+
private impersonateEmail;
|
|
49
|
+
private baseUrl;
|
|
50
|
+
private jwtClient;
|
|
51
|
+
private cachedToken;
|
|
52
|
+
private tokenExpiry;
|
|
53
|
+
private refreshPromise;
|
|
54
|
+
constructor(credentials: ServiceAccountCredentials, impersonateEmail: string);
|
|
55
|
+
private refreshToken;
|
|
56
|
+
private getHeaders;
|
|
57
|
+
listMessages(options?: {
|
|
58
|
+
q?: string;
|
|
59
|
+
maxResults?: number;
|
|
60
|
+
pageToken?: string;
|
|
61
|
+
labelIds?: string[];
|
|
62
|
+
}): Promise<{
|
|
63
|
+
messages: EmailListItem[];
|
|
64
|
+
nextPageToken?: string;
|
|
65
|
+
resultSizeEstimate?: number;
|
|
66
|
+
}>;
|
|
67
|
+
getMessage(messageId: string, options?: {
|
|
68
|
+
format?: 'minimal' | 'full' | 'raw' | 'metadata';
|
|
69
|
+
metadataHeaders?: string[];
|
|
70
|
+
}): Promise<Email>;
|
|
71
|
+
}
|
|
72
|
+
export type ClientFactory = () => IGmailClient;
|
|
73
|
+
/**
|
|
74
|
+
* Creates the default Gmail client based on environment variables.
|
|
75
|
+
* Uses service account with domain-wide delegation:
|
|
76
|
+
* - GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address
|
|
77
|
+
* - GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)
|
|
78
|
+
* - GMAIL_IMPERSONATE_EMAIL: Email address to impersonate
|
|
79
|
+
*/
|
|
80
|
+
export declare function createDefaultClient(): IGmailClient;
|
|
81
|
+
export declare function createMCPServer(): {
|
|
82
|
+
server: Server<{
|
|
83
|
+
method: string;
|
|
84
|
+
params?: {
|
|
85
|
+
[x: string]: unknown;
|
|
86
|
+
_meta?: {
|
|
87
|
+
[x: string]: unknown;
|
|
88
|
+
progressToken?: string | number | undefined;
|
|
89
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
90
|
+
taskId: string;
|
|
91
|
+
} | undefined;
|
|
92
|
+
} | undefined;
|
|
93
|
+
} | undefined;
|
|
94
|
+
}, {
|
|
95
|
+
method: string;
|
|
96
|
+
params?: {
|
|
97
|
+
[x: string]: unknown;
|
|
98
|
+
_meta?: {
|
|
99
|
+
[x: string]: unknown;
|
|
100
|
+
progressToken?: string | number | undefined;
|
|
101
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
102
|
+
taskId: string;
|
|
103
|
+
} | undefined;
|
|
104
|
+
} | undefined;
|
|
105
|
+
} | undefined;
|
|
106
|
+
}, {
|
|
107
|
+
[x: string]: unknown;
|
|
108
|
+
_meta?: {
|
|
109
|
+
[x: string]: unknown;
|
|
110
|
+
progressToken?: string | number | undefined;
|
|
111
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
112
|
+
taskId: string;
|
|
113
|
+
} | undefined;
|
|
114
|
+
} | undefined;
|
|
115
|
+
}>;
|
|
116
|
+
registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
|
|
117
|
+
};
|
package/shared/server.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { JWT } from 'google-auth-library';
|
|
3
|
+
import { createRegisterTools } from './tools.js';
|
|
4
|
+
/**
|
|
5
|
+
* Gmail API client implementation using service account with domain-wide delegation
|
|
6
|
+
*/
|
|
7
|
+
export class ServiceAccountGmailClient {
|
|
8
|
+
impersonateEmail;
|
|
9
|
+
baseUrl = 'https://gmail.googleapis.com/gmail/v1/users/me';
|
|
10
|
+
jwtClient;
|
|
11
|
+
cachedToken = null;
|
|
12
|
+
tokenExpiry = 0;
|
|
13
|
+
refreshPromise = null;
|
|
14
|
+
constructor(credentials, impersonateEmail) {
|
|
15
|
+
this.impersonateEmail = impersonateEmail;
|
|
16
|
+
this.jwtClient = new JWT({
|
|
17
|
+
email: credentials.client_email,
|
|
18
|
+
key: credentials.private_key,
|
|
19
|
+
scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
|
|
20
|
+
subject: impersonateEmail,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async refreshToken() {
|
|
24
|
+
const tokenResponse = await this.jwtClient.authorize();
|
|
25
|
+
if (!tokenResponse.access_token) {
|
|
26
|
+
throw new Error('Failed to obtain access token from service account');
|
|
27
|
+
}
|
|
28
|
+
this.cachedToken = tokenResponse.access_token;
|
|
29
|
+
// Token typically expires in 1 hour, but use the actual expiry if provided
|
|
30
|
+
this.tokenExpiry = tokenResponse.expiry_date || Date.now() + 3600000;
|
|
31
|
+
}
|
|
32
|
+
async getHeaders() {
|
|
33
|
+
// Check if we have a valid cached token (with 60 second buffer)
|
|
34
|
+
if (this.cachedToken && Date.now() < this.tokenExpiry - 60000) {
|
|
35
|
+
return {
|
|
36
|
+
Authorization: `Bearer ${this.cachedToken}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Use mutex pattern to prevent concurrent token refresh
|
|
41
|
+
if (!this.refreshPromise) {
|
|
42
|
+
this.refreshPromise = this.refreshToken().finally(() => {
|
|
43
|
+
this.refreshPromise = null;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
await this.refreshPromise;
|
|
47
|
+
return {
|
|
48
|
+
Authorization: `Bearer ${this.cachedToken}`,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async listMessages(options) {
|
|
53
|
+
const headers = await this.getHeaders();
|
|
54
|
+
const { listMessages } = await import('./gmail-client/lib/list-messages.js');
|
|
55
|
+
return listMessages(this.baseUrl, headers, options);
|
|
56
|
+
}
|
|
57
|
+
async getMessage(messageId, options) {
|
|
58
|
+
const headers = await this.getHeaders();
|
|
59
|
+
const { getMessage } = await import('./gmail-client/lib/get-message.js');
|
|
60
|
+
return getMessage(this.baseUrl, headers, messageId, options);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates the default Gmail client based on environment variables.
|
|
65
|
+
* Uses service account with domain-wide delegation:
|
|
66
|
+
* - GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL: Service account email address
|
|
67
|
+
* - GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY: Service account private key (PEM format)
|
|
68
|
+
* - GMAIL_IMPERSONATE_EMAIL: Email address to impersonate
|
|
69
|
+
*/
|
|
70
|
+
export function createDefaultClient() {
|
|
71
|
+
const clientEmail = process.env.GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL;
|
|
72
|
+
// Handle both literal \n in JSON configs and actual newlines
|
|
73
|
+
const privateKey = process.env.GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY?.replace(/\\n/g, '\n');
|
|
74
|
+
const impersonateEmail = process.env.GMAIL_IMPERSONATE_EMAIL;
|
|
75
|
+
if (!clientEmail) {
|
|
76
|
+
throw new Error('GMAIL_SERVICE_ACCOUNT_CLIENT_EMAIL environment variable must be set. ' +
|
|
77
|
+
'This is the email address from your Google Cloud service account.');
|
|
78
|
+
}
|
|
79
|
+
if (!privateKey) {
|
|
80
|
+
throw new Error('GMAIL_SERVICE_ACCOUNT_PRIVATE_KEY environment variable must be set. ' +
|
|
81
|
+
'This is the private key from your Google Cloud service account (PEM format).');
|
|
82
|
+
}
|
|
83
|
+
if (!impersonateEmail) {
|
|
84
|
+
throw new Error('GMAIL_IMPERSONATE_EMAIL environment variable must be set. ' +
|
|
85
|
+
'This is the email address of the user to access Gmail as.');
|
|
86
|
+
}
|
|
87
|
+
const credentials = {
|
|
88
|
+
type: 'service_account',
|
|
89
|
+
project_id: '',
|
|
90
|
+
private_key_id: '',
|
|
91
|
+
private_key: privateKey,
|
|
92
|
+
client_email: clientEmail,
|
|
93
|
+
client_id: '',
|
|
94
|
+
auth_uri: 'https://accounts.google.com/o/oauth2/auth',
|
|
95
|
+
token_uri: 'https://oauth2.googleapis.com/token',
|
|
96
|
+
auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs',
|
|
97
|
+
client_x509_cert_url: '',
|
|
98
|
+
};
|
|
99
|
+
return new ServiceAccountGmailClient(credentials, impersonateEmail);
|
|
100
|
+
}
|
|
101
|
+
export function createMCPServer() {
|
|
102
|
+
const server = new Server({
|
|
103
|
+
name: 'gmail-workspace-mcp-server',
|
|
104
|
+
version: '0.0.2',
|
|
105
|
+
}, {
|
|
106
|
+
capabilities: {
|
|
107
|
+
tools: {},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const registerHandlers = async (server, clientFactory) => {
|
|
111
|
+
// Use provided factory or create default client
|
|
112
|
+
const factory = clientFactory || createDefaultClient;
|
|
113
|
+
const registerTools = createRegisterTools(factory);
|
|
114
|
+
registerTools(server);
|
|
115
|
+
};
|
|
116
|
+
return { server, registerHandlers };
|
|
117
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ClientFactory } from '../server.js';
|
|
4
|
+
export declare const GetEmailSchema: z.ZodObject<{
|
|
5
|
+
email_id: z.ZodString;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
email_id: string;
|
|
8
|
+
}, {
|
|
9
|
+
email_id: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function getEmailTool(server: Server, clientFactory: ClientFactory): {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object";
|
|
16
|
+
properties: {
|
|
17
|
+
email_id: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: string[];
|
|
23
|
+
};
|
|
24
|
+
handler: (args: unknown) => Promise<{
|
|
25
|
+
content: {
|
|
26
|
+
type: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}[];
|
|
29
|
+
isError?: undefined;
|
|
30
|
+
} | {
|
|
31
|
+
content: {
|
|
32
|
+
type: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
isError: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getHeader } from '../utils/email-helpers.js';
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
email_id: 'The unique identifier of the email to retrieve. ' +
|
|
5
|
+
'Obtain this from gmail_list_recent_emails.',
|
|
6
|
+
};
|
|
7
|
+
export const GetEmailSchema = z.object({
|
|
8
|
+
email_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.email_id),
|
|
9
|
+
});
|
|
10
|
+
const TOOL_DESCRIPTION = `Retrieve the full content of a specific email by its ID.
|
|
11
|
+
|
|
12
|
+
Returns the complete email including headers, body content, and attachment information.
|
|
13
|
+
|
|
14
|
+
**Parameters:**
|
|
15
|
+
- email_id: The unique identifier of the email (required)
|
|
16
|
+
|
|
17
|
+
**Returns:**
|
|
18
|
+
Full email details including:
|
|
19
|
+
- Subject, From, To, Cc, Date headers
|
|
20
|
+
- Full message body (plain text preferred, HTML as fallback)
|
|
21
|
+
- List of attachments (if any)
|
|
22
|
+
- Labels assigned to the email
|
|
23
|
+
|
|
24
|
+
**Use cases:**
|
|
25
|
+
- Read the full content of an email after listing recent emails
|
|
26
|
+
- Extract specific information from an email body
|
|
27
|
+
- Check attachment details
|
|
28
|
+
|
|
29
|
+
**Note:** Use gmail_list_recent_emails first to get email IDs.`;
|
|
30
|
+
/**
|
|
31
|
+
* Decodes base64url encoded content
|
|
32
|
+
*/
|
|
33
|
+
function decodeBase64Url(data) {
|
|
34
|
+
// Convert base64url to base64
|
|
35
|
+
const base64 = data.replace(/-/g, '+').replace(/_/g, '/');
|
|
36
|
+
// Decode from base64
|
|
37
|
+
return Buffer.from(base64, 'base64').toString('utf-8');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Recursively extracts body content from email parts
|
|
41
|
+
* Prefers text/plain over text/html
|
|
42
|
+
*/
|
|
43
|
+
function extractBodyContent(parts, preferredType = 'text/plain') {
|
|
44
|
+
if (!parts)
|
|
45
|
+
return null;
|
|
46
|
+
// First pass: look for exact match
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (part.mimeType === preferredType && part.body?.data) {
|
|
49
|
+
return decodeBase64Url(part.body.data);
|
|
50
|
+
}
|
|
51
|
+
if (part.parts) {
|
|
52
|
+
const nested = extractBodyContent(part.parts, preferredType);
|
|
53
|
+
if (nested)
|
|
54
|
+
return nested;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Gets the body content from an email
|
|
61
|
+
*/
|
|
62
|
+
function getEmailBody(email) {
|
|
63
|
+
// Check if body is directly on payload
|
|
64
|
+
if (email.payload?.body?.data) {
|
|
65
|
+
return decodeBase64Url(email.payload.body.data);
|
|
66
|
+
}
|
|
67
|
+
// Try to extract from parts
|
|
68
|
+
if (email.payload?.parts) {
|
|
69
|
+
// Prefer plain text
|
|
70
|
+
const plainText = extractBodyContent(email.payload.parts, 'text/plain');
|
|
71
|
+
if (plainText)
|
|
72
|
+
return plainText;
|
|
73
|
+
// Fall back to HTML
|
|
74
|
+
const html = extractBodyContent(email.payload.parts, 'text/html');
|
|
75
|
+
if (html) {
|
|
76
|
+
// Strip HTML tags for readability
|
|
77
|
+
return html
|
|
78
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
79
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
80
|
+
.replace(/<[^>]+>/g, ' ')
|
|
81
|
+
.replace(/\s+/g, ' ')
|
|
82
|
+
.trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return '(No body content available)';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extracts attachment information from email parts
|
|
89
|
+
*/
|
|
90
|
+
function getAttachments(parts) {
|
|
91
|
+
if (!parts)
|
|
92
|
+
return [];
|
|
93
|
+
const attachments = [];
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (part.filename && part.body?.attachmentId) {
|
|
96
|
+
attachments.push({
|
|
97
|
+
filename: part.filename,
|
|
98
|
+
mimeType: part.mimeType,
|
|
99
|
+
size: part.body.size,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (part.parts) {
|
|
103
|
+
attachments.push(...getAttachments(part.parts));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return attachments;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Formats an email for display
|
|
110
|
+
*/
|
|
111
|
+
function formatFullEmail(email) {
|
|
112
|
+
const subject = getHeader(email, 'Subject') || '(No Subject)';
|
|
113
|
+
const from = getHeader(email, 'From') || 'Unknown';
|
|
114
|
+
const to = getHeader(email, 'To') || 'Unknown';
|
|
115
|
+
const cc = getHeader(email, 'Cc');
|
|
116
|
+
const date = getHeader(email, 'Date') || 'Unknown date';
|
|
117
|
+
const body = getEmailBody(email);
|
|
118
|
+
const attachments = getAttachments(email.payload?.parts);
|
|
119
|
+
const labels = email.labelIds?.join(', ') || 'None';
|
|
120
|
+
let output = `# Email Details
|
|
121
|
+
|
|
122
|
+
**ID:** ${email.id}
|
|
123
|
+
**Thread ID:** ${email.threadId}
|
|
124
|
+
|
|
125
|
+
## Headers
|
|
126
|
+
**Subject:** ${subject}
|
|
127
|
+
**From:** ${from}
|
|
128
|
+
**To:** ${to}`;
|
|
129
|
+
if (cc) {
|
|
130
|
+
output += `\n**Cc:** ${cc}`;
|
|
131
|
+
}
|
|
132
|
+
output += `
|
|
133
|
+
**Date:** ${date}
|
|
134
|
+
**Labels:** ${labels}
|
|
135
|
+
|
|
136
|
+
## Body
|
|
137
|
+
|
|
138
|
+
${body}`;
|
|
139
|
+
if (attachments.length > 0) {
|
|
140
|
+
output += `\n\n## Attachments (${attachments.length})\n`;
|
|
141
|
+
attachments.forEach((att, i) => {
|
|
142
|
+
const sizeKb = Math.round(att.size / 1024);
|
|
143
|
+
output += `${i + 1}. ${att.filename} (${att.mimeType}, ${sizeKb} KB)\n`;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
export function getEmailTool(server, clientFactory) {
|
|
149
|
+
return {
|
|
150
|
+
name: 'gmail_get_email',
|
|
151
|
+
description: TOOL_DESCRIPTION,
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
email_id: {
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: PARAM_DESCRIPTIONS.email_id,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
required: ['email_id'],
|
|
161
|
+
},
|
|
162
|
+
handler: async (args) => {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = GetEmailSchema.parse(args ?? {});
|
|
165
|
+
const client = clientFactory();
|
|
166
|
+
const email = await client.getMessage(parsed.email_id, {
|
|
167
|
+
format: 'full',
|
|
168
|
+
});
|
|
169
|
+
const formattedEmail = formatFullEmail(email);
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: 'text',
|
|
174
|
+
text: formattedEmail,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
text: `Error retrieving email: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
isError: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ClientFactory } from '../server.js';
|
|
4
|
+
export declare const ListRecentEmailsSchema: z.ZodObject<{
|
|
5
|
+
hours: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
labels: z.ZodDefault<z.ZodOptional<z.ZodString>>;
|
|
7
|
+
max_results: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
hours: number;
|
|
10
|
+
labels: string;
|
|
11
|
+
max_results: number;
|
|
12
|
+
}, {
|
|
13
|
+
hours?: number | undefined;
|
|
14
|
+
labels?: string | undefined;
|
|
15
|
+
max_results?: number | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function listRecentEmailsTool(server: Server, clientFactory: ClientFactory): {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object";
|
|
22
|
+
properties: {
|
|
23
|
+
hours: {
|
|
24
|
+
type: string;
|
|
25
|
+
default: number;
|
|
26
|
+
description: string;
|
|
27
|
+
};
|
|
28
|
+
labels: {
|
|
29
|
+
type: string;
|
|
30
|
+
default: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
max_results: {
|
|
34
|
+
type: string;
|
|
35
|
+
default: number;
|
|
36
|
+
description: "Maximum number of emails to return. Default: 10. Max: 100.";
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
required: never[];
|
|
40
|
+
};
|
|
41
|
+
handler: (args: unknown) => Promise<{
|
|
42
|
+
content: {
|
|
43
|
+
type: string;
|
|
44
|
+
text: string;
|
|
45
|
+
}[];
|
|
46
|
+
isError?: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
content: {
|
|
49
|
+
type: string;
|
|
50
|
+
text: string;
|
|
51
|
+
}[];
|
|
52
|
+
isError: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getHeader } from '../utils/email-helpers.js';
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
hours: 'Time horizon in hours to look back for emails. Default: 24. ' +
|
|
5
|
+
'Example: 48 for the last 2 days.',
|
|
6
|
+
labels: 'Comma-separated list of label IDs to filter by. Default: INBOX. ' +
|
|
7
|
+
'Common labels: INBOX, SENT, DRAFTS, SPAM, TRASH, STARRED, IMPORTANT, UNREAD.',
|
|
8
|
+
max_results: 'Maximum number of emails to return. Default: 10. Max: 100.',
|
|
9
|
+
};
|
|
10
|
+
export const ListRecentEmailsSchema = z.object({
|
|
11
|
+
hours: z.number().positive().default(24).describe(PARAM_DESCRIPTIONS.hours),
|
|
12
|
+
labels: z.string().optional().default('INBOX').describe(PARAM_DESCRIPTIONS.labels),
|
|
13
|
+
max_results: z.number().positive().max(100).default(10).describe(PARAM_DESCRIPTIONS.max_results),
|
|
14
|
+
});
|
|
15
|
+
const TOOL_DESCRIPTION = `List recent emails from Gmail within a specified time horizon.
|
|
16
|
+
|
|
17
|
+
Returns a list of recent emails with their subject, sender, date, and a snippet preview. Use get_email to retrieve the full content of a specific email.
|
|
18
|
+
|
|
19
|
+
**Parameters:**
|
|
20
|
+
- hours: How far back to look for emails (default: 24 hours)
|
|
21
|
+
- labels: Which labels/folders to search (default: INBOX)
|
|
22
|
+
- max_results: Maximum emails to return (default: 10, max: 100)
|
|
23
|
+
|
|
24
|
+
**Returns:**
|
|
25
|
+
A formatted list of emails with:
|
|
26
|
+
- Email ID (needed for get_email)
|
|
27
|
+
- Subject line
|
|
28
|
+
- Sender (From)
|
|
29
|
+
- Date received
|
|
30
|
+
- Snippet preview
|
|
31
|
+
|
|
32
|
+
**Use cases:**
|
|
33
|
+
- Check recent inbox activity
|
|
34
|
+
- Monitor for new emails in a time window
|
|
35
|
+
- List recent emails from specific labels like SENT or STARRED
|
|
36
|
+
|
|
37
|
+
**Note:** This tool only returns email metadata and snippets. Use get_email with an email ID to retrieve the full message content.`;
|
|
38
|
+
/**
|
|
39
|
+
* Formats an email for display
|
|
40
|
+
*/
|
|
41
|
+
function formatEmail(email) {
|
|
42
|
+
const subject = getHeader(email, 'Subject') || '(No Subject)';
|
|
43
|
+
const from = getHeader(email, 'From') || 'Unknown';
|
|
44
|
+
const date = getHeader(email, 'Date') || 'Unknown date';
|
|
45
|
+
const snippet = email.snippet || '';
|
|
46
|
+
return `**ID:** ${email.id}
|
|
47
|
+
**Subject:** ${subject}
|
|
48
|
+
**From:** ${from}
|
|
49
|
+
**Date:** ${date}
|
|
50
|
+
**Preview:** ${snippet}`;
|
|
51
|
+
}
|
|
52
|
+
export function listRecentEmailsTool(server, clientFactory) {
|
|
53
|
+
return {
|
|
54
|
+
name: 'gmail_list_recent_emails',
|
|
55
|
+
description: TOOL_DESCRIPTION,
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
hours: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
default: 24,
|
|
62
|
+
description: PARAM_DESCRIPTIONS.hours,
|
|
63
|
+
},
|
|
64
|
+
labels: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
default: 'INBOX',
|
|
67
|
+
description: PARAM_DESCRIPTIONS.labels,
|
|
68
|
+
},
|
|
69
|
+
max_results: {
|
|
70
|
+
type: 'number',
|
|
71
|
+
default: 10,
|
|
72
|
+
description: PARAM_DESCRIPTIONS.max_results,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: [],
|
|
76
|
+
},
|
|
77
|
+
handler: async (args) => {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = ListRecentEmailsSchema.parse(args ?? {});
|
|
80
|
+
const client = clientFactory();
|
|
81
|
+
// Calculate the timestamp for the time horizon
|
|
82
|
+
const now = new Date();
|
|
83
|
+
const cutoffDate = new Date(now.getTime() - parsed.hours * 60 * 60 * 1000);
|
|
84
|
+
const afterTimestamp = Math.floor(cutoffDate.getTime() / 1000);
|
|
85
|
+
// Build the Gmail query
|
|
86
|
+
const query = `after:${afterTimestamp}`;
|
|
87
|
+
// Parse labels
|
|
88
|
+
const labelIds = parsed.labels.split(',').map((l) => l.trim().toUpperCase());
|
|
89
|
+
// List messages
|
|
90
|
+
const { messages } = await client.listMessages({
|
|
91
|
+
q: query,
|
|
92
|
+
maxResults: parsed.max_results,
|
|
93
|
+
labelIds,
|
|
94
|
+
});
|
|
95
|
+
if (messages.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: `No emails found in the last ${parsed.hours} hour(s) with labels: ${labelIds.join(', ')}`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Fetch full details for each message
|
|
106
|
+
const emailDetails = await Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
107
|
+
format: 'metadata',
|
|
108
|
+
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
109
|
+
})));
|
|
110
|
+
const formattedEmails = emailDetails.map(formatEmail).join('\n\n---\n\n');
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: `Found ${messages.length} email(s) in the last ${parsed.hours} hour(s):\n\n${formattedEmails}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: `Error listing emails: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
isError: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { ClientFactory } from './server.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a function to register all tools with the server
|
|
5
|
+
*/
|
|
6
|
+
export declare function createRegisterTools(clientFactory: ClientFactory): (server: Server) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Backward compatibility export
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerTools(server: Server): void;
|
package/shared/tools.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { listRecentEmailsTool } from './tools/list-recent-emails.js';
|
|
3
|
+
import { getEmailTool } from './tools/get-email.js';
|
|
4
|
+
/**
|
|
5
|
+
* All available tools
|
|
6
|
+
*/
|
|
7
|
+
const ALL_TOOLS = [listRecentEmailsTool, getEmailTool];
|
|
8
|
+
/**
|
|
9
|
+
* Creates a function to register all tools with the server
|
|
10
|
+
*/
|
|
11
|
+
export function createRegisterTools(clientFactory) {
|
|
12
|
+
return (server) => {
|
|
13
|
+
// Create tool instances
|
|
14
|
+
const tools = ALL_TOOLS.map((factory) => factory(server, clientFactory));
|
|
15
|
+
// List available tools
|
|
16
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
17
|
+
return {
|
|
18
|
+
tools: tools.map((tool) => ({
|
|
19
|
+
name: tool.name,
|
|
20
|
+
description: tool.description,
|
|
21
|
+
inputSchema: tool.inputSchema,
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
// Handle tool calls
|
|
26
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
27
|
+
const { name, arguments: args } = request.params;
|
|
28
|
+
const tool = tools.find((t) => t.name === name);
|
|
29
|
+
if (!tool) {
|
|
30
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
31
|
+
}
|
|
32
|
+
return await tool.handler(args);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Backward compatibility export
|
|
38
|
+
*/
|
|
39
|
+
export function registerTools(server) {
|
|
40
|
+
const factory = () => {
|
|
41
|
+
throw new Error('No client factory provided - use createRegisterTools for dependency injection');
|
|
42
|
+
};
|
|
43
|
+
const register = createRegisterTools(factory);
|
|
44
|
+
register(server);
|
|
45
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail API Types
|
|
3
|
+
* Based on Gmail API responses
|
|
4
|
+
*/
|
|
5
|
+
export interface EmailHeader {
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EmailPart {
|
|
10
|
+
partId: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
filename?: string;
|
|
13
|
+
headers?: EmailHeader[];
|
|
14
|
+
body?: {
|
|
15
|
+
attachmentId?: string;
|
|
16
|
+
size: number;
|
|
17
|
+
data?: string;
|
|
18
|
+
};
|
|
19
|
+
parts?: EmailPart[];
|
|
20
|
+
}
|
|
21
|
+
export interface Email {
|
|
22
|
+
id: string;
|
|
23
|
+
threadId: string;
|
|
24
|
+
labelIds?: string[];
|
|
25
|
+
snippet: string;
|
|
26
|
+
historyId: string;
|
|
27
|
+
internalDate: string;
|
|
28
|
+
payload?: {
|
|
29
|
+
partId?: string;
|
|
30
|
+
mimeType: string;
|
|
31
|
+
filename?: string;
|
|
32
|
+
headers?: EmailHeader[];
|
|
33
|
+
body?: {
|
|
34
|
+
attachmentId?: string;
|
|
35
|
+
size: number;
|
|
36
|
+
data?: string;
|
|
37
|
+
};
|
|
38
|
+
parts?: EmailPart[];
|
|
39
|
+
};
|
|
40
|
+
sizeEstimate?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface EmailListItem {
|
|
43
|
+
id: string;
|
|
44
|
+
threadId: string;
|
|
45
|
+
}
|
|
46
|
+
export interface Label {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
messageListVisibility?: 'show' | 'hide';
|
|
50
|
+
labelListVisibility?: 'labelShow' | 'labelShowIfUnread' | 'labelHide';
|
|
51
|
+
type?: 'system' | 'user';
|
|
52
|
+
color?: {
|
|
53
|
+
textColor: string;
|
|
54
|
+
backgroundColor: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export interface Thread {
|
|
58
|
+
id: string;
|
|
59
|
+
historyId: string;
|
|
60
|
+
messages?: Email[];
|
|
61
|
+
}
|
|
62
|
+
export interface PaginatedResponse<T> {
|
|
63
|
+
items: T[];
|
|
64
|
+
nextPageToken?: string;
|
|
65
|
+
resultSizeEstimate?: number;
|
|
66
|
+
}
|
package/shared/types.js
ADDED