gmcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ // src/auth.ts
2
+ import { google } from "googleapis";
3
+
4
+ // src/types.ts
5
+ import { z } from "zod";
6
+ var OAuth2CredentialsSchema = z.object({
7
+ installed: z.object({
8
+ client_id: z.string().min(1, "client_id is required"),
9
+ project_id: z.string().min(1, "project_id is required"),
10
+ auth_uri: z.string().url("auth_uri must be a valid URL"),
11
+ token_uri: z.string().url("token_uri must be a valid URL"),
12
+ auth_provider_x509_cert_url: z.string().url("auth_provider_x509_cert_url must be a valid URL"),
13
+ client_secret: z.string().min(1, "client_secret is required"),
14
+ redirect_uris: z.array(z.string().url("redirect_uris must contain valid URLs"))
15
+ })
16
+ });
17
+ var StoredTokensSchema = z.object({
18
+ access_token: z.string().min(1, "access_token is required"),
19
+ refresh_token: z.string().min(1, "refresh_token is required"),
20
+ scope: z.string().min(1, "scope is required"),
21
+ token_type: z.string().min(1, "token_type is required"),
22
+ expiry_date: z.number().int().positive("expiry_date must be a positive integer")
23
+ });
24
+ var DEFAULT_GMAIL_SCOPE = "https://www.googleapis.com/auth/gmail.readonly";
25
+ var GMAIL_SCOPE_MAP = {
26
+ "gmail.readonly": DEFAULT_GMAIL_SCOPE,
27
+ "gmail.modify": "https://www.googleapis.com/auth/gmail.modify",
28
+ "gmail.send": "https://www.googleapis.com/auth/gmail.send",
29
+ "gmail.labels": "https://www.googleapis.com/auth/gmail.labels",
30
+ "gmail.metadata": "https://www.googleapis.com/auth/gmail.metadata",
31
+ "gmail.compose": "https://www.googleapis.com/auth/gmail.compose",
32
+ "gmail.insert": "https://www.googleapis.com/auth/gmail.insert",
33
+ "gmail.settings.basic": "https://www.googleapis.com/auth/gmail.settings.basic",
34
+ "gmail.settings.sharing": "https://www.googleapis.com/auth/gmail.settings.sharing"
35
+ };
36
+ var DEFAULT_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
37
+ var CALENDAR_SCOPE_MAP = {
38
+ "calendar.readonly": DEFAULT_CALENDAR_SCOPE,
39
+ calendar: "https://www.googleapis.com/auth/calendar",
40
+ "calendar.events": "https://www.googleapis.com/auth/calendar.events",
41
+ "calendar.events.readonly": "https://www.googleapis.com/auth/calendar.events.readonly",
42
+ "calendar.settings.readonly": "https://www.googleapis.com/auth/calendar.settings.readonly",
43
+ "calendar.calendarlist.readonly": "https://www.googleapis.com/auth/calendar.calendarlist.readonly",
44
+ "calendar.calendarlist": "https://www.googleapis.com/auth/calendar.calendarlist"
45
+ };
46
+ function parseScopes(scopesEnv) {
47
+ if (!scopesEnv) {
48
+ return [DEFAULT_GMAIL_SCOPE];
49
+ }
50
+ return scopesEnv.split(",").map((scope) => {
51
+ const trimmed = scope.trim();
52
+ if (GMAIL_SCOPE_MAP[trimmed]) {
53
+ return GMAIL_SCOPE_MAP[trimmed];
54
+ }
55
+ if (CALENDAR_SCOPE_MAP[trimmed]) {
56
+ return CALENDAR_SCOPE_MAP[trimmed];
57
+ }
58
+ return trimmed;
59
+ });
60
+ }
61
+ function getHeader(headers, name) {
62
+ if (!headers) {
63
+ return "";
64
+ }
65
+ const header = headers.find((h) => h.name?.toLowerCase() === name.toLowerCase());
66
+ return header?.value || "";
67
+ }
68
+
69
+ // src/auth.ts
70
+ async function loadCredentials(path) {
71
+ try {
72
+ const file = Bun.file(path);
73
+ const content = await file.text();
74
+ const parsed = JSON.parse(content);
75
+ return OAuth2CredentialsSchema.parse(parsed);
76
+ } catch (error) {
77
+ throw new Error(`Failed to load credentials from ${path}: ${error}`);
78
+ }
79
+ }
80
+ async function loadTokens(path, logger) {
81
+ try {
82
+ const file = Bun.file(path);
83
+ const exists = await file.exists();
84
+ if (!exists) {
85
+ return null;
86
+ }
87
+ const content = await file.text();
88
+ const parsed = JSON.parse(content);
89
+ return StoredTokensSchema.parse(parsed);
90
+ } catch (error) {
91
+ logger?.warn({
92
+ path,
93
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
94
+ error: error instanceof Error ? error.message : String(error)
95
+ }, "Failed to load tokens");
96
+ return null;
97
+ }
98
+ }
99
+ async function saveTokens(path, tokens) {
100
+ try {
101
+ await Bun.write(path, JSON.stringify(tokens, null, 2));
102
+ } catch (error) {
103
+ throw new Error(`Failed to save tokens to ${path}: ${error}`);
104
+ }
105
+ }
106
+ function createOAuth2Client(credentials) {
107
+ const { client_id, client_secret, redirect_uris } = credentials.installed;
108
+ return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
109
+ }
110
+ function getAuthUrl(oauth2Client, scopes) {
111
+ return oauth2Client.generateAuthUrl({
112
+ access_type: "offline",
113
+ scope: scopes,
114
+ prompt: "consent"
115
+ });
116
+ }
117
+ async function getTokensFromCode(oauth2Client, code) {
118
+ const { tokens } = await oauth2Client.getToken(code);
119
+ if (!(tokens.access_token && tokens.refresh_token)) {
120
+ throw new Error("Failed to obtain access_token or refresh_token");
121
+ }
122
+ return {
123
+ access_token: tokens.access_token,
124
+ refresh_token: tokens.refresh_token,
125
+ scope: tokens.scope || "",
126
+ token_type: tokens.token_type || "Bearer",
127
+ expiry_date: tokens.expiry_date || Date.now() + 3600 * 1000
128
+ };
129
+ }
130
+ async function createAuthenticatedClient(credentialsPath, tokenPath, logger) {
131
+ const credentials = await loadCredentials(credentialsPath);
132
+ const oauth2Client = createOAuth2Client(credentials);
133
+ const tokens = await loadTokens(tokenPath, logger);
134
+ if (!tokens) {
135
+ throw new Error(`No tokens found at ${tokenPath}. Please run 'bun run auth' to authenticate first.`);
136
+ }
137
+ oauth2Client.setCredentials(tokens);
138
+ oauth2Client.on("tokens", async (newTokens) => {
139
+ const updatedTokens = {
140
+ ...tokens,
141
+ access_token: newTokens.access_token || tokens.access_token,
142
+ expiry_date: newTokens.expiry_date || tokens.expiry_date
143
+ };
144
+ if (newTokens.refresh_token) {
145
+ updatedTokens.refresh_token = newTokens.refresh_token;
146
+ }
147
+ logger?.info({
148
+ expiry_date: updatedTokens.expiry_date,
149
+ has_refresh_token: !!updatedTokens.refresh_token
150
+ }, "Tokens refreshed");
151
+ await saveTokens(tokenPath, updatedTokens);
152
+ });
153
+ return oauth2Client;
154
+ }
155
+ function getEnvConfig() {
156
+ const credentialsPath = process.env.GOOGLE_CREDENTIALS_PATH;
157
+ const tokenPath = process.env.GOOGLE_TOKEN_PATH;
158
+ const scopesEnv = process.env.GOOGLE_SCOPES;
159
+ if (!credentialsPath) {
160
+ throw new Error("GOOGLE_CREDENTIALS_PATH environment variable is required");
161
+ }
162
+ if (!tokenPath) {
163
+ throw new Error("GOOGLE_TOKEN_PATH environment variable is required");
164
+ }
165
+ const scopes = parseScopes(scopesEnv);
166
+ return { credentialsPath, tokenPath, scopes };
167
+ }
168
+
169
+ export { getHeader, loadCredentials, saveTokens, createOAuth2Client, getAuthUrl, getTokensFromCode, createAuthenticatedClient, getEnvConfig };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "gmcp",
3
+ "version": "0.1.0",
4
+ "description": "GMCP Server for Google Workspace with OAuth2 authentication",
5
+ "module": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "type": "module",
9
+ "bin": {
10
+ "gmcp": "dist/index.js",
11
+ "gmcp-auth": "dist/auth-cli.js"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "start": "bun run src/index.ts",
24
+ "auth": "bun run src/auth-cli.ts",
25
+ "build": "bunup",
26
+ "prepublishOnly": "bun run build",
27
+ "prepare": "lefthook install"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/johnie/gmcp.git"
32
+ },
33
+ "author": "Johnie Hjelm",
34
+ "license": "MIT",
35
+ "keywords": [
36
+ "mcp",
37
+ "gmail",
38
+ "calendar",
39
+ "google",
40
+ "oauth2",
41
+ "anthropic"
42
+ ],
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.25.3",
45
+ "googleapis": "^170.1.0",
46
+ "json2md": "^2.0.3",
47
+ "pino": "^9.7.0",
48
+ "pino-pretty": "^13.0.0",
49
+ "zod": "^4.3.6"
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "2.3.12",
53
+ "@types/bun": "latest",
54
+ "@types/json2md": "^1.5.4",
55
+ "bunup": "^0.16.20",
56
+ "lefthook": "^2.0.15",
57
+ "ultracite": "7.0.12",
58
+ "vitest": "^4.0.18"
59
+ },
60
+ "peerDependencies": {
61
+ "typescript": "^5"
62
+ }
63
+ }