gogcli-mcp-contacts 2.8.0 → 2.18.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/dist/index.js +19894 -19588
- package/manifest.json +14 -2
- package/package.json +3 -2
- package/src/index.ts +7 -9
- package/src/tools/contacts-extra.ts +12 -5
- package/tests/tools/contacts-extra.test.ts +52 -34
- package/tsconfig.json +1 -1
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-contacts",
|
|
5
5
|
"display_name": "gogcli (Contacts)",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.18.0",
|
|
7
7
|
"description": "Extended Google Contacts for Claude via gogcli — auth + Contacts + Workspace directory (People API)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -58,6 +58,18 @@
|
|
|
58
58
|
"name": "gog_auth_add",
|
|
59
59
|
"description": "Authorize a Google account via browser-based OAuth"
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
"name": "gog_auth_health",
|
|
63
|
+
"description": "Live per-account token health: validity, age, and pre-expiry warnings (detects invalid_grant)"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "gog_auth_add_url",
|
|
67
|
+
"description": "Begin remote/headless OAuth (step 1): return a sign-in URL to open in any browser"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "gog_auth_add_complete",
|
|
71
|
+
"description": "Complete remote/headless OAuth (step 2): exchange the pasted redirect URL and store the token"
|
|
72
|
+
},
|
|
61
73
|
{
|
|
62
74
|
"name": "gog_auth_list",
|
|
63
75
|
"description": "List all Google accounts stored in gogcli"
|
|
@@ -128,7 +140,7 @@
|
|
|
128
140
|
},
|
|
129
141
|
{
|
|
130
142
|
"name": "gog_contacts_dedupe",
|
|
131
|
-
"description": "Find
|
|
143
|
+
"description": "Find duplicate contacts; optionally merge and delete redundants"
|
|
132
144
|
},
|
|
133
145
|
{
|
|
134
146
|
"name": "gog_contacts_directory_list",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-contacts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-contacts",
|
|
5
5
|
"description": "Extended Google Contacts + People MCP server via gogcli — auth + Contacts + Workspace directory (People API)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"build": "tsc --noEmit && npm run bundle",
|
|
20
20
|
"bundle": "node ../../scripts/bundle.js src/index.ts dist/index.js",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"test": "vitest run",
|
|
22
|
+
"test": "vitest run --coverage",
|
|
23
23
|
"test:watch": "vitest",
|
|
24
24
|
"test:coverage": "vitest run --coverage"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@chrischall/mcp-utils": "^0.13.3",
|
|
27
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
29
|
"zod": "^4.4.3"
|
|
29
30
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { runMcp } from '@chrischall/mcp-utils';
|
|
3
|
+
import { VERSION, authToolsFor, registerContactsTools } from '../../gogcli-mcp/src/lib.js';
|
|
4
4
|
import { registerExtraContactsTools } from './tools/contacts-extra.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const transport = new StdioServerTransport();
|
|
12
|
-
await server.connect(transport);
|
|
6
|
+
await runMcp({
|
|
7
|
+
name: 'gogcli-contacts',
|
|
8
|
+
version: VERSION,
|
|
9
|
+
tools: [authToolsFor('contacts'), registerContactsTools, registerExtraContactsTools],
|
|
10
|
+
});
|
|
@@ -100,7 +100,7 @@ export function registerExtraContactsTools(server: McpServer): void {
|
|
|
100
100
|
account: accountParam,
|
|
101
101
|
},
|
|
102
102
|
}, async ({ resourceName, account }) => {
|
|
103
|
-
return runOrDiagnose(['contacts', 'delete', resourceName], { account });
|
|
103
|
+
return runOrDiagnose(['contacts', 'delete', resourceName, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
server.registerTool('gog_contacts_export', {
|
|
@@ -127,17 +127,24 @@ export function registerExtraContactsTools(server: McpServer): void {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
server.registerTool('gog_contacts_dedupe', {
|
|
130
|
-
description: 'Find likely duplicate personal contacts
|
|
131
|
-
annotations: {
|
|
130
|
+
description: 'Find likely duplicate personal contacts. Defaults to a read-only preview of the merge plan; set apply to actually merge each duplicate group and delete the redundant contacts (etag-checked, with ambiguous or unmergeable groups refused). Scope a risky apply with resource. Always preview first.',
|
|
131
|
+
annotations: { destructiveHint: true },
|
|
132
132
|
inputSchema: {
|
|
133
133
|
match: z.string().optional().describe('Match fields, comma-separated from email,phone,name (default: email,phone)'),
|
|
134
|
-
max: z.number().optional().describe('Max contacts to scan (0 = all)'),
|
|
134
|
+
max: z.number().optional().describe('Max contacts to scan (0 = all). Mutually exclusive with resource — gog rejects passing both.'),
|
|
135
|
+
resource: z.array(z.string()).optional().describe('Limit dedupe to these exact contact resource names (e.g. people/c123), repeatable — useful to scope an apply to known duplicates. Mutually exclusive with max.'),
|
|
136
|
+
apply: z.boolean().optional().describe('Merge each duplicate group and DELETE the redundant contacts. Without this the tool only previews the plan. Destructive — preview first.'),
|
|
137
|
+
failEmpty: z.boolean().optional().describe('Exit with an error (code 3) when no duplicates are found, instead of succeeding empty.'),
|
|
135
138
|
account: accountParam,
|
|
136
139
|
},
|
|
137
|
-
}, async ({ match, max, account }) => {
|
|
140
|
+
}, async ({ match, max, resource, apply, failEmpty, account }) => {
|
|
138
141
|
const args = ['contacts', 'dedupe'];
|
|
139
142
|
if (match) args.push(`--match=${match}`);
|
|
140
143
|
if (max !== undefined) args.push(`--max=${max}`);
|
|
144
|
+
if (resource) for (const r of resource) args.push(`--resource=${r}`);
|
|
145
|
+
if (apply) args.push('--apply');
|
|
146
|
+
if (failEmpty) args.push('--fail-empty');
|
|
147
|
+
if (apply) args.push('--force'); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
141
148
|
return runOrDiagnose(args, { account });
|
|
142
149
|
});
|
|
143
150
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { registerExtraContactsTools } from '../../src/tools/contacts-extra.js';
|
|
3
3
|
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createTestHarness, type TestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
5
6
|
|
|
6
7
|
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
7
8
|
const actual = await importOriginal<typeof lib>();
|
|
@@ -11,41 +12,41 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
|
11
12
|
};
|
|
12
13
|
});
|
|
13
14
|
|
|
14
|
-
let
|
|
15
|
+
let harness: TestHarness;
|
|
15
16
|
|
|
16
|
-
beforeEach(() => {
|
|
17
|
+
beforeEach(async () => {
|
|
17
18
|
vi.clearAllMocks();
|
|
18
|
-
vi.mocked(lib.runOrDiagnose).mockResolvedValue(
|
|
19
|
-
|
|
19
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
20
|
+
harness = await createTestHarness(registerExtraContactsTools);
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
describe('gog_people_me', () => {
|
|
23
24
|
it('calls runOrDiagnose with people me', async () => {
|
|
24
|
-
await
|
|
25
|
+
await harness.callTool('gog_people_me', {});
|
|
25
26
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'me'], { account: undefined });
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
it('forwards account', async () => {
|
|
29
|
-
await
|
|
30
|
+
await harness.callTool('gog_people_me', { account: 'a@b.com' });
|
|
30
31
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'me'], { account: 'a@b.com' });
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
describe('gog_people_get', () => {
|
|
35
36
|
it('calls runOrDiagnose with userId', async () => {
|
|
36
|
-
await
|
|
37
|
+
await harness.callTool('gog_people_get', { userId: 'people/c123' });
|
|
37
38
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'get', 'people/c123'], { account: undefined });
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
describe('gog_people_search', () => {
|
|
42
43
|
it('calls runOrDiagnose with query', async () => {
|
|
43
|
-
await
|
|
44
|
+
await harness.callTool('gog_people_search', { query: 'alice' });
|
|
44
45
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'search', 'alice'], { account: undefined });
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
it('passes pagination flags', async () => {
|
|
48
|
-
await
|
|
49
|
+
await harness.callTool('gog_people_search', { query: 'alice', max: 100, page: 'tok', all: true });
|
|
49
50
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
50
51
|
['people', 'search', 'alice', '--max=100', '--page=tok', '--all'],
|
|
51
52
|
{ account: undefined },
|
|
@@ -53,19 +54,19 @@ describe('gog_people_search', () => {
|
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it('omits --all when false', async () => {
|
|
56
|
-
await
|
|
57
|
+
await harness.callTool('gog_people_search', { query: 'x', all: false });
|
|
57
58
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'search', 'x'], { account: undefined });
|
|
58
59
|
});
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
describe('gog_people_relations', () => {
|
|
62
63
|
it('calls runOrDiagnose with no userId', async () => {
|
|
63
|
-
await
|
|
64
|
+
await harness.callTool('gog_people_relations', {});
|
|
64
65
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'relations'], { account: undefined });
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
it('passes userId and --type when provided', async () => {
|
|
68
|
-
await
|
|
69
|
+
await harness.callTool('gog_people_relations', { userId: 'people/c123', type: 'manager' });
|
|
69
70
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
70
71
|
['people', 'relations', 'people/c123', '--type=manager'],
|
|
71
72
|
{ account: undefined },
|
|
@@ -75,12 +76,12 @@ describe('gog_people_relations', () => {
|
|
|
75
76
|
|
|
76
77
|
describe('gog_contacts_update', () => {
|
|
77
78
|
it('calls runOrDiagnose with just resourceName', async () => {
|
|
78
|
-
await
|
|
79
|
+
await harness.callTool('gog_contacts_update', { resourceName: 'people/c1' });
|
|
79
80
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'update', 'people/c1'], { account: undefined });
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
it('passes all fields including empty-string clears', async () => {
|
|
83
|
-
await
|
|
84
|
+
await harness.callTool('gog_contacts_update', {
|
|
84
85
|
resourceName: 'people/c1',
|
|
85
86
|
given: 'Ada',
|
|
86
87
|
family: 'Lovelace',
|
|
@@ -106,26 +107,26 @@ describe('gog_contacts_update', () => {
|
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
it('omits --ignore-etag when false', async () => {
|
|
109
|
-
await
|
|
110
|
+
await harness.callTool('gog_contacts_update', { resourceName: 'people/c1', ignoreEtag: false });
|
|
110
111
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'update', 'people/c1'], { account: undefined });
|
|
111
112
|
});
|
|
112
113
|
});
|
|
113
114
|
|
|
114
115
|
describe('gog_contacts_delete', () => {
|
|
115
116
|
it('calls runOrDiagnose with resourceName', async () => {
|
|
116
|
-
await
|
|
117
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'delete', 'people/c1'], { account: undefined });
|
|
117
|
+
await harness.callTool('gog_contacts_delete', { resourceName: 'people/c1' });
|
|
118
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'delete', 'people/c1', '--force'], { account: undefined });
|
|
118
119
|
});
|
|
119
120
|
});
|
|
120
121
|
|
|
121
122
|
describe('gog_contacts_export', () => {
|
|
122
123
|
it('calls runOrDiagnose with no options', async () => {
|
|
123
|
-
await
|
|
124
|
+
await harness.callTool('gog_contacts_export', {});
|
|
124
125
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'export'], { account: undefined });
|
|
125
126
|
});
|
|
126
127
|
|
|
127
128
|
it('passes selector and all flags', async () => {
|
|
128
|
-
await
|
|
129
|
+
await harness.callTool('gog_contacts_export', {
|
|
129
130
|
selector: 'people/c1',
|
|
130
131
|
query: 'ada',
|
|
131
132
|
all: true,
|
|
@@ -140,34 +141,51 @@ describe('gog_contacts_export', () => {
|
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
it('omits --all when false', async () => {
|
|
143
|
-
await
|
|
144
|
+
await harness.callTool('gog_contacts_export', { all: false });
|
|
144
145
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'export'], { account: undefined });
|
|
145
146
|
});
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
describe('gog_contacts_dedupe', () => {
|
|
149
150
|
it('calls runOrDiagnose with no options', async () => {
|
|
150
|
-
await
|
|
151
|
+
await harness.callTool('gog_contacts_dedupe', {});
|
|
151
152
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'dedupe'], { account: undefined });
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
it('passes --match and --max', async () => {
|
|
155
|
-
await
|
|
156
|
+
await harness.callTool('gog_contacts_dedupe', { match: 'name', max: 100 });
|
|
156
157
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
157
158
|
['contacts', 'dedupe', '--match=name', '--max=100'],
|
|
158
159
|
{ account: undefined },
|
|
159
160
|
);
|
|
160
161
|
});
|
|
162
|
+
|
|
163
|
+
it('passes --apply, repeatable --resource, and --fail-empty', async () => {
|
|
164
|
+
await harness.callTool('gog_contacts_dedupe', {
|
|
165
|
+
apply: true,
|
|
166
|
+
resource: ['people/c1', 'people/c2'],
|
|
167
|
+
failEmpty: true,
|
|
168
|
+
});
|
|
169
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
170
|
+
['contacts', 'dedupe', '--resource=people/c1', '--resource=people/c2', '--apply', '--fail-empty', '--force'],
|
|
171
|
+
{ account: undefined },
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('omits --apply and --fail-empty when false', async () => {
|
|
176
|
+
await harness.callTool('gog_contacts_dedupe', { apply: false, failEmpty: false });
|
|
177
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'dedupe'], { account: undefined });
|
|
178
|
+
});
|
|
161
179
|
});
|
|
162
180
|
|
|
163
181
|
describe('gog_contacts_directory_list', () => {
|
|
164
182
|
it('calls runOrDiagnose with no options', async () => {
|
|
165
|
-
await
|
|
183
|
+
await harness.callTool('gog_contacts_directory_list', {});
|
|
166
184
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'directory', 'list'], { account: undefined });
|
|
167
185
|
});
|
|
168
186
|
|
|
169
187
|
it('passes pagination flags', async () => {
|
|
170
|
-
await
|
|
188
|
+
await harness.callTool('gog_contacts_directory_list', { max: 50, page: 'tok', all: true });
|
|
171
189
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
172
190
|
['contacts', 'directory', 'list', '--max=50', '--page=tok', '--all'],
|
|
173
191
|
{ account: undefined },
|
|
@@ -175,19 +193,19 @@ describe('gog_contacts_directory_list', () => {
|
|
|
175
193
|
});
|
|
176
194
|
|
|
177
195
|
it('omits --all when false', async () => {
|
|
178
|
-
await
|
|
196
|
+
await harness.callTool('gog_contacts_directory_list', { all: false });
|
|
179
197
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'directory', 'list'], { account: undefined });
|
|
180
198
|
});
|
|
181
199
|
});
|
|
182
200
|
|
|
183
201
|
describe('gog_contacts_other_list', () => {
|
|
184
202
|
it('calls runOrDiagnose with no options', async () => {
|
|
185
|
-
await
|
|
203
|
+
await harness.callTool('gog_contacts_other_list', {});
|
|
186
204
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'other', 'list'], { account: undefined });
|
|
187
205
|
});
|
|
188
206
|
|
|
189
207
|
it('passes pagination flags', async () => {
|
|
190
|
-
await
|
|
208
|
+
await harness.callTool('gog_contacts_other_list', { max: 100, page: 'tok', all: true });
|
|
191
209
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
192
210
|
['contacts', 'other', 'list', '--max=100', '--page=tok', '--all'],
|
|
193
211
|
{ account: undefined },
|
|
@@ -195,19 +213,19 @@ describe('gog_contacts_other_list', () => {
|
|
|
195
213
|
});
|
|
196
214
|
|
|
197
215
|
it('omits --all when false', async () => {
|
|
198
|
-
await
|
|
216
|
+
await harness.callTool('gog_contacts_other_list', { all: false });
|
|
199
217
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'other', 'list'], { account: undefined });
|
|
200
218
|
});
|
|
201
219
|
});
|
|
202
220
|
|
|
203
221
|
describe('gog_contacts_other_search', () => {
|
|
204
222
|
it('calls runOrDiagnose with query', async () => {
|
|
205
|
-
await
|
|
223
|
+
await harness.callTool('gog_contacts_other_search', { query: 'ada' });
|
|
206
224
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'other', 'search', 'ada'], { account: undefined });
|
|
207
225
|
});
|
|
208
226
|
|
|
209
227
|
it('passes --max when provided', async () => {
|
|
210
|
-
await
|
|
228
|
+
await harness.callTool('gog_contacts_other_search', { query: 'ada', max: 25 });
|
|
211
229
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
212
230
|
['contacts', 'other', 'search', 'ada', '--max=25'],
|
|
213
231
|
{ account: undefined },
|
|
@@ -217,12 +235,12 @@ describe('gog_contacts_other_search', () => {
|
|
|
217
235
|
|
|
218
236
|
describe('gog_people_raw', () => {
|
|
219
237
|
it('calls runOrDiagnose with userId', async () => {
|
|
220
|
-
await
|
|
238
|
+
await harness.callTool('gog_people_raw', { userId: 'people/c123' });
|
|
221
239
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'raw', 'people/c123'], { account: undefined });
|
|
222
240
|
});
|
|
223
241
|
|
|
224
242
|
it('passes --person-fields and --pretty when provided', async () => {
|
|
225
|
-
await
|
|
243
|
+
await harness.callTool('gog_people_raw', {
|
|
226
244
|
userId: 'people/c123',
|
|
227
245
|
personFields: 'names,emailAddresses',
|
|
228
246
|
pretty: true,
|
|
@@ -234,7 +252,7 @@ describe('gog_people_raw', () => {
|
|
|
234
252
|
});
|
|
235
253
|
|
|
236
254
|
it('omits --pretty when false', async () => {
|
|
237
|
-
await
|
|
255
|
+
await harness.callTool('gog_people_raw', { userId: 'people/c123', pretty: false });
|
|
238
256
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'raw', 'people/c123'], { account: undefined });
|
|
239
257
|
});
|
|
240
258
|
});
|
package/tsconfig.json
CHANGED