gogcli-mcp-contacts 4.5.2 → 4.6.1

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/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": "4.5.2",
6
+ "version": "4.6.1",
7
7
  "description": "Extended Google Contacts for Claude via gogcli — auth + Contacts + Workspace directory (People API)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -160,6 +160,22 @@
160
160
  {
161
161
  "name": "gog_contacts_other_search",
162
162
  "description": "Search auto-collected other contacts"
163
+ },
164
+ {
165
+ "name": "gog_contacts_batch_get",
166
+ "description": "Fetch exact Contacts in native People API batches."
167
+ },
168
+ {
169
+ "name": "gog_contacts_batch_create",
170
+ "description": "Create Contacts in native People API batches."
171
+ },
172
+ {
173
+ "name": "gog_contacts_batch_update",
174
+ "description": "Update Contacts in native People API batches with etag guards."
175
+ },
176
+ {
177
+ "name": "gog_contacts_batch_delete",
178
+ "description": "Permanently delete Contacts in native People API batches after confirmation."
163
179
  }
164
180
  ],
165
181
  "compatibility": {
package/mint.yaml CHANGED
@@ -92,7 +92,7 @@ dependencies:
92
92
  # bumping these nine pins, and the pin stored on each mcp-host registration.
93
93
  - kind: github-release
94
94
  repo: openclaw/gogcli
95
- tag: v0.41.0
95
+ tag: v0.43.0
96
96
  asset: "gogcli_*_linux_amd64.tar.gz"
97
97
  bin: [gog]
98
98
  state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-contacts",
3
- "version": "4.5.2",
3
+ "version": "4.6.1",
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>",
@@ -24,7 +24,7 @@
24
24
  "test:coverage": "vitest run --coverage"
25
25
  },
26
26
  "dependencies": {
27
- "@chrischall/mcp-utils": "^2.6.0",
27
+ "@chrischall/mcp-utils": "^2.13.0",
28
28
  "@modelcontextprotocol/server": "^2.1.0",
29
29
  "zod": "^4.6.1"
30
30
  },
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose, pageTokenParam, pageAliasParam, resolvePageToken, pos, confinePath } from '../../../gogcli-mcp/src/lib.js';
3
+ import { accountParam, runOrDiagnose, pageTokenParam, pageAliasParam, resolvePageToken, pos, confinePath, confirmTokenParam, requireDispatchConfirmation } from '../../../gogcli-mcp/src/lib.js';
4
4
  import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
5
5
 
6
6
  // People is the richer API behind Google Contacts: Workspace directory
@@ -107,6 +107,52 @@ export function registerExtraContactsTools(server: McpServer): void {
107
107
  return runOrDiagnose(['contacts', 'delete', pos(resourceName), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
108
108
  });
109
109
 
110
+ server.registerTool('gog_contacts_batch_get', {
111
+ description: 'Fetch exact Google Contacts by People resource name in native API batches (up to 200).',
112
+ annotations: { readOnlyHint: true },
113
+ inputSchema: z.object({ resourceNames: z.array(z.string().min(1)).min(1).max(200).describe('Exact resource names such as people/c123; maximum 200'), account: accountParam }),
114
+ }, async ({ resourceNames, account }) => runOrDiagnose(['contacts', 'batch', 'get', ...resourceNames.map(pos)], { account }));
115
+
116
+ server.registerTool('gog_contacts_batch_create', {
117
+ description: 'Create Google Contacts in native People API batches (up to 200). peopleJson is a JSON array of People API Person resources. The payload is materialized in a temporary file on the gog host.',
118
+ // Same kind of write as gog_contacts_create, so the same annotation.
119
+ annotations: { readOnlyHint: false, destructiveHint: true },
120
+ inputSchema: z.object({ peopleJson: z.string().describe('JSON array of People API Person resources, up to 200 contacts'), account: accountParam }),
121
+ }, async ({ peopleJson, account }) => {
122
+ let people: unknown;
123
+ try { people = JSON.parse(peopleJson); } catch { throw new Error('peopleJson must be valid JSON'); }
124
+ if (!Array.isArray(people) || people.length === 0 || people.length > 200) throw new Error('peopleJson must be a non-empty array with at most 200 contacts');
125
+ return runOrDiagnose(['contacts', 'batch', 'create', { kind: 'file', flag: 'from-file', contents: peopleJson, ext: 'json' }], { account });
126
+ });
127
+
128
+ server.registerTool('gog_contacts_batch_update', {
129
+ description: 'Update Google Contacts in native People API batches. peopleByResourceName is a JSON object keyed by People resource name; each value must include the CONTACT source metadata and etag returned by a prior read. gog preserves those etag guards and reports partial progress.',
130
+ // Overwrites fields like gog_contacts_update, so the same annotation.
131
+ annotations: { readOnlyHint: false, destructiveHint: true },
132
+ inputSchema: z.object({ peopleByResourceName: z.string().describe('JSON object keyed by people/... resource name, with Person resources including CONTACT source metadata and etag'), account: accountParam }),
133
+ }, async ({ peopleByResourceName, account }) => {
134
+ let people: unknown;
135
+ try { people = JSON.parse(peopleByResourceName); } catch { throw new Error('peopleByResourceName must be valid JSON'); }
136
+ if (!people || typeof people !== 'object' || Array.isArray(people) || Object.keys(people).length === 0 || Object.keys(people).length > 200) throw new Error('peopleByResourceName must be a JSON object with between 1 and 200 contacts');
137
+ return runOrDiagnose(['contacts', 'batch', 'update', { kind: 'file', flag: 'from-file', contents: peopleByResourceName, ext: 'json' }], { account });
138
+ });
139
+
140
+ server.registerTool('gog_contacts_batch_delete', {
141
+ description: 'Permanently delete exact Google Contacts in native People API batches (up to 500). Requires explicit user confirmation; deleted contacts cannot be restored from Trash.',
142
+ annotations: { destructiveHint: true },
143
+ inputSchema: z.object({ resourceNames: z.array(z.string().min(1)).min(1).max(500).describe('Exact People resource names; maximum 500'), account: accountParam, confirmToken: confirmTokenParam }),
144
+ }, async ({ resourceNames, account, confirmToken }, ctx) => {
145
+ const confirmation = await requireDispatchConfirmation(ctx, {
146
+ action: 'contacts.batch-delete',
147
+ message: 'Review and confirm permanent deletion of these Google Contacts:',
148
+ confirmationLabel: 'Confirm permanent deletion of the listed contacts.',
149
+ details: { resourceNames },
150
+ fallback: { tool: 'gog_contacts_batch_delete', account, confirmToken, subject: () => ({ target: 'contacts', payload: { resourceNames }, preview: { resourceNames } }) },
151
+ });
152
+ if (confirmation) return confirmation;
153
+ return runOrDiagnose(['contacts', 'batch', 'delete', ...resourceNames.map(pos), '--force'], { account });
154
+ });
155
+
110
156
  server.registerTool('gog_contacts_export', {
111
157
  description: 'Export contacts as vCard (.vcf). Provide a selector (resource name, email, or name), or use query / all to export multiple.',
112
158
  // Writes a file on the gog host and can overwrite one: not read-only (audit SEC-4).
@@ -120,6 +120,73 @@ describe('gog_contacts_delete', () => {
120
120
  });
121
121
  });
122
122
 
123
+ describe('native batched Contacts tools', () => {
124
+ it('fetches exact resource names in one command', async () => {
125
+ await harness.callTool('gog_contacts_batch_get', { resourceNames: ['people/a', 'people/b'] });
126
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'batch', 'get', pos('people/a'), pos('people/b')], { account: undefined });
127
+ });
128
+
129
+ it('materializes a validated create array as a JSON file argument', async () => {
130
+ const peopleJson = '[{"names":[{"givenName":"Ada"}]}]';
131
+ await harness.callTool('gog_contacts_batch_create', { peopleJson });
132
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith([
133
+ 'contacts', 'batch', 'create', { kind: 'file', flag: 'from-file', contents: peopleJson, ext: 'json' },
134
+ ], { account: undefined });
135
+ });
136
+
137
+ it('rejects invalid create JSON before invoking gog', async () => {
138
+ const result = await harness.callTool('gog_contacts_batch_create', { peopleJson: '{' });
139
+ expect(result.isError).toBe(true);
140
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it('rejects an empty or non-array create payload', async () => {
144
+ expect((await harness.callTool('gog_contacts_batch_create', { peopleJson: '{}' })).isError).toBe(true);
145
+ expect((await harness.callTool('gog_contacts_batch_create', { peopleJson: '[]' })).isError).toBe(true);
146
+ expect((await harness.callTool('gog_contacts_batch_create', { peopleJson: JSON.stringify(Array(201).fill({})) })).isError).toBe(true);
147
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('materializes an update map and preserves the account', async () => {
151
+ const peopleByResourceName = '{"people/a":{"etag":"x"}}';
152
+ await harness.callTool('gog_contacts_batch_update', { peopleByResourceName, account: 'a@b.com' });
153
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith([
154
+ 'contacts', 'batch', 'update', { kind: 'file', flag: 'from-file', contents: peopleByResourceName, ext: 'json' },
155
+ ], { account: 'a@b.com' });
156
+ });
157
+
158
+ it('rejects invalid or empty update maps', async () => {
159
+ expect((await harness.callTool('gog_contacts_batch_update', { peopleByResourceName: '{' })).isError).toBe(true);
160
+ expect((await harness.callTool('gog_contacts_batch_update', { peopleByResourceName: '[]' })).isError).toBe(true);
161
+ expect((await harness.callTool('gog_contacts_batch_update', { peopleByResourceName: '{}' })).isError).toBe(true);
162
+ const tooMany = Object.fromEntries(Array.from({ length: 201 }, (_, i) => [`people/${i}`, {}]));
163
+ expect((await harness.callTool('gog_contacts_batch_update', { peopleByResourceName: JSON.stringify(tooMany) })).isError).toBe(true);
164
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
165
+ });
166
+
167
+ it('requires confirmation and then forces the guarded permanent delete', async () => {
168
+ const confirmed = await createTestHarness(registerExtraContactsTools, {
169
+ elicitation: async () => ({ action: 'accept', content: { confirmed: true } }),
170
+ });
171
+ await confirmed.callTool('gog_contacts_batch_delete', { resourceNames: ['people/a', 'people/b'] });
172
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith([
173
+ 'contacts', 'batch', 'delete', pos('people/a'), pos('people/b'), '--force',
174
+ ], { account: undefined });
175
+ });
176
+
177
+ it('allows an unprompted client to confirm the exact batch with a one-time token', async () => {
178
+ process.env.MCP_CONFIRM_MODE = 'ask-user';
179
+ const unprompted = await createTestHarness(registerExtraContactsTools);
180
+ const args = { resourceNames: ['people/a'] };
181
+ const prompt = await unprompted.callTool('gog_contacts_batch_delete', args);
182
+ const payload = JSON.parse(prompt.content[0].text);
183
+ await unprompted.callTool('gog_contacts_batch_delete', { ...args, confirmToken: payload.confirmToken });
184
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
185
+ ['contacts', 'batch', 'delete', pos('people/a'), '--force'], { account: undefined });
186
+ delete process.env.MCP_CONFIRM_MODE;
187
+ });
188
+ });
189
+
123
190
  describe('gog_contacts_export', () => {
124
191
  it('calls runOrDiagnose with no options', async () => {
125
192
  await harness.callTool('gog_contacts_export', {});