gogcli-mcp-contacts 2.30.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.30.0",
6
+ "version": "4.0.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",
package/mint.yaml CHANGED
@@ -12,25 +12,29 @@ env:
12
12
  - name: GOG_CLIENT_ID
13
13
  required: false
14
14
  help: >-
15
- Google OAuth client id. NOTE: on the local-spawn path the child `gog`
16
- receives a sanitized env — runner.ts drops GOG_ACCESS_TOKEN and every
17
- *_TOKEN / *_SECRET / *_KEY / *_CREDENTIALS variable — so the CLI
18
- authenticates from its own stored credentials under $HOME (see
19
- state.dataDir), not from these variables being passed through.
15
+ Google OAuth client id. Read by the Node wrapper at startup
16
+ (bootstrap-auth.ts), which imports it into gog's keyring via a temp file
17
+ together with GOG_CLIENT_SECRET, GOG_REFRESH_TOKEN and GOG_ACCOUNT — set
18
+ all four or none. Not a secret, so unlike GOG_CLIENT_SECRET and
19
+ GOG_REFRESH_TOKEN it is not stripped from the spawned `gog`'s
20
+ environment; gog authenticates from its keyring under $HOME (see
21
+ state.dataDir).
20
22
  - name: GOG_CLIENT_SECRET
21
23
  secret: true
22
24
  required: false
23
25
  help: >-
24
- Google OAuth client secret. Stripped from the spawned CLI's environment
25
- by runner.ts's *_SECRET rule — see GOG_CLIENT_ID.
26
+ Google OAuth client secret. Imported into gog's keyring at startup with
27
+ GOG_CLIENT_ID; stripped from the spawned CLI's environment by runner.ts's
28
+ *_SECRET rule.
26
29
  - name: GOG_REFRESH_TOKEN
27
30
  secret: true
28
31
  required: false
29
32
  help: >-
30
- Google OAuth refresh token. Stripped from the spawned CLI's environment
31
- by runner.ts's *_TOKEN rule — see GOG_CLIENT_ID. A hosted deployment must
32
- therefore carry gog's authorised-account state in its persisted data dir,
33
- or drive a remote runner via GOG_RUNNER_URL.
33
+ Google OAuth refresh token (`gog auth tokens export` prints one). Imported
34
+ into gog's keyring at startup with GOG_CLIENT_ID; stripped from the
35
+ spawned CLI's environment by runner.ts's *_TOKEN rule. Changing it
36
+ re-imports on the next start; an in-connector re-auth
37
+ (gog_auth_add_url/complete) stays in effect until it changes.
34
38
  - name: GOG_ACCESS_TOKEN
35
39
  secret: true
36
40
  required: false
@@ -41,8 +45,21 @@ env:
41
45
  - name: GOG_ACCOUNT
42
46
  required: false
43
47
  help: >-
44
- Which configured Google account to act as, when more than one is
45
- authorised. Defaults to the single/most recent account.
48
+ The Google account to act as. Required for the startup auth bootstrap
49
+ (the refresh token is imported under this email); otherwise defaults to
50
+ gog's single/most recent account.
51
+ - name: GOG_KEYRING_BACKEND
52
+ required: false
53
+ help: >-
54
+ gog's credential store. Set to "file" on a headless host — there is no OS
55
+ keychain — so the keyring lives on the persistent data dir.
56
+ - name: GOG_KEYRING_PASSWORD
57
+ secret: true
58
+ required: false
59
+ help: >-
60
+ Encrypts gog's file keyring on the data dir. Required with
61
+ GOG_KEYRING_BACKEND=file. Deliberately not stripped from the spawned
62
+ CLI's environment — it is the one credential gog itself reads.
46
63
  - name: GOG_READONLY
47
64
  required: false
48
65
  help: >-
@@ -53,17 +70,6 @@ env:
53
70
  help: >-
54
71
  Path to the `gog` binary. Leave unset when the dependency below supplies
55
72
  it; set it only to point at a binary you manage yourself.
56
- - name: GOG_RUNNER_URL
57
- required: false
58
- help: >-
59
- URL of a remote gog runner to execute against instead of spawning the
60
- local binary. If you set this, add its host to egress.allow.
61
- - name: GOG_RUNNER_KEY
62
- secret: true
63
- required: false
64
- help: >-
65
- Shared key authenticating calls to GOG_RUNNER_URL. Required whenever that
66
- is set.
67
73
  - name: GOG_TIMEZONE
68
74
  required: false
69
75
  help: >-
@@ -82,9 +88,8 @@ dependencies:
82
88
  # Every tool shells out to the `gog` CLI; without it the server starts and
83
89
  # then fails on the first call. This tag must track
84
90
  # packages/gogcli-mcp/src/runner.ts's MIN_GOG_VERSION (the floor the tools
85
- # assume) and the fly-gog-runner/Dockerfile GOG_VERSION build arg. See
86
- # CLAUDE.md "Required gog version" — bumping the floor means bumping these
87
- # nine pins too.
91
+ # assume). See CLAUDE.md "Required gog version" — bumping the floor means
92
+ # bumping these nine pins, and the pin stored on each mcp-host registration.
88
93
  - kind: github-release
89
94
  repo: openclaw/gogcli
90
95
  tag: v0.40.0
@@ -93,15 +98,14 @@ dependencies:
93
98
  state:
94
99
  dataDir: true
95
100
  reason: >-
96
- `gog` keeps its authorised-account state and token cache under $HOME.
97
- Without a persistent data dir every cold start has no account configured and
98
- every tool call fails until the credentials are re-supplied.
101
+ `gog` keeps its authorised-account state and file keyring under $HOME, and
102
+ the wrapper keeps its auth-bootstrap marker (~/.gogcli-mcp) there, so a
103
+ restart neither re-imports an unchanged secret nor discards an
104
+ in-connector re-auth. Without a persistent data dir every cold start has
105
+ only what the env secrets can re-seed.
99
106
  egress:
100
107
  allow:
101
108
  # Google OAuth token exchange and the Google REST APIs the CLI calls.
102
109
  - oauth2.googleapis.com
103
110
  - www.googleapis.com
104
111
  - googleapis.com
105
- #
106
- # NOTE: if you set GOG_RUNNER_URL to run against a remote gog runner, add
107
- # that host here too — it is supplied at runtime and cannot be declared.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-contacts",
3
- "version": "2.30.0",
3
+ "version": "4.0.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>",
@@ -24,8 +24,8 @@
24
24
  "test:coverage": "vitest run --coverage"
25
25
  },
26
26
  "dependencies": {
27
- "@chrischall/mcp-utils": "^0.23.3",
28
- "@modelcontextprotocol/sdk": "^1.30.0",
27
+ "@chrischall/mcp-utils": "^0.28.0",
28
+ "@modelcontextprotocol/server": "^2.0.0",
29
29
  "zod": "^4.6.1"
30
30
  },
31
31
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { runMcp } from '@chrischall/mcp-utils';
3
- import { VERSION, authToolsFor, registerContactsTools, useRemoteGogRunner } from '../../gogcli-mcp/src/lib.js';
3
+ import { VERSION, authToolsFor, registerContactsTools, bootstrapGogAuth } from '../../gogcli-mcp/src/lib.js';
4
4
  import { registerExtraContactsTools } from './tools/contacts-extra.js';
5
5
 
6
- // Execute `gog` on the Fly backend when the host points us at one; without
7
- // it, nothing changes and we spawn the local binary as before.
8
- useRemoteGogRunner();
6
+
7
+ // Seed gog's keyring from GOG_CLIENT_ID/SECRET/REFRESH_TOKEN/ACCOUNT when the host injects them.
8
+ await bootstrapGogAuth();
9
9
 
10
10
  await runMcp({
11
11
  name: 'gogcli-contacts',
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { accountParam, runOrDiagnose, pageTokenParam, pageAliasParam, resolvePageToken} from '../../../gogcli-mcp/src/lib.js';
4
4
 
@@ -8,9 +8,9 @@ export function registerExtraContactsTools(server: McpServer): void {
8
8
  server.registerTool('gog_people_me', {
9
9
  description: 'Show your own People profile (people/me).',
10
10
  annotations: { readOnlyHint: true },
11
- inputSchema: {
11
+ inputSchema: z.object({
12
12
  account: accountParam,
13
- },
13
+ }),
14
14
  }, async ({ account }) => {
15
15
  return runOrDiagnose(['people', 'me'], { account });
16
16
  });
@@ -18,10 +18,10 @@ export function registerExtraContactsTools(server: McpServer): void {
18
18
  server.registerTool('gog_people_get', {
19
19
  description: 'Get a People profile by resource name.',
20
20
  annotations: { readOnlyHint: true },
21
- inputSchema: {
21
+ inputSchema: z.object({
22
22
  userId: z.string().describe('Person resource name (people/...) or email'),
23
23
  account: accountParam,
24
- },
24
+ }),
25
25
  }, async ({ userId, account }) => {
26
26
  return runOrDiagnose(['people', 'get', userId], { account });
27
27
  });
@@ -29,14 +29,14 @@ export function registerExtraContactsTools(server: McpServer): void {
29
29
  server.registerTool('gog_people_search', {
30
30
  description: 'Search the Google Workspace directory (covers internal users, unlike contacts search which is limited to your personal contacts).',
31
31
  annotations: { readOnlyHint: true },
32
- inputSchema: {
32
+ inputSchema: z.object({
33
33
  query: z.string().describe('Search query (name, email, etc.)'),
34
34
  max: z.number().optional().describe('Max results (default: 50)'),
35
35
  pageToken: pageTokenParam,
36
36
  page: pageAliasParam,
37
37
  all: z.boolean().optional().describe('Fetch all pages'),
38
38
  account: accountParam,
39
- },
39
+ }),
40
40
  }, async ({ query, max, pageToken, page, all, account }) => {
41
41
  const args = ['people', 'search', query];
42
42
  if (max !== undefined) args.push(`--max=${max}`);
@@ -49,11 +49,11 @@ export function registerExtraContactsTools(server: McpServer): void {
49
49
  server.registerTool('gog_people_relations', {
50
50
  description: 'Get relations (manager, reports, etc.) for a user. Defaults to self when userId is omitted.',
51
51
  annotations: { readOnlyHint: true },
52
- inputSchema: {
52
+ inputSchema: z.object({
53
53
  userId: z.string().optional().describe('Person resource name (defaults to self when omitted)'),
54
54
  type: z.string().optional().describe('Filter to a specific relation type (e.g. "manager")'),
55
55
  account: accountParam,
56
- },
56
+ }),
57
57
  }, async ({ userId, type, account }) => {
58
58
  const args = ['people', 'relations'];
59
59
  if (userId) args.push(userId);
@@ -63,7 +63,7 @@ export function registerExtraContactsTools(server: McpServer): void {
63
63
 
64
64
  server.registerTool('gog_contacts_update', {
65
65
  description: 'Update an existing Google Contact. Empty string clears a field; repeatable fields (url/address/custom/relation) take comma/semicolon-separated lists.',
66
- inputSchema: {
66
+ inputSchema: z.object({
67
67
  resourceName: z.string().describe('Contact resource name (people/...)'),
68
68
  given: z.string().optional().describe('Given (first) name'),
69
69
  family: z.string().optional().describe('Family (last) name'),
@@ -77,7 +77,7 @@ export function registerExtraContactsTools(server: McpServer): void {
77
77
  birthday: z.string().optional().describe('Birthday in YYYY-MM-DD (empty string clears)'),
78
78
  ignoreEtag: z.boolean().optional().describe('Allow update even if a supplied etag is stale (may overwrite concurrent changes)'),
79
79
  account: accountParam,
80
- },
80
+ }),
81
81
  }, async ({ resourceName, given, family, email, phone, org, title, url, note, address, birthday, ignoreEtag, account }) => {
82
82
  const args = ['contacts', 'update', resourceName];
83
83
  if (given !== undefined) args.push(`--given=${given}`);
@@ -97,10 +97,10 @@ export function registerExtraContactsTools(server: McpServer): void {
97
97
  server.registerTool('gog_contacts_delete', {
98
98
  description: 'Delete a Google Contact by resource name.',
99
99
  annotations: { destructiveHint: true },
100
- inputSchema: {
100
+ inputSchema: z.object({
101
101
  resourceName: z.string().describe('Contact resource name (people/...)'),
102
102
  account: accountParam,
103
- },
103
+ }),
104
104
  }, async ({ resourceName, account }) => {
105
105
  return runOrDiagnose(['contacts', 'delete', resourceName, '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
106
106
  });
@@ -108,7 +108,7 @@ export function registerExtraContactsTools(server: McpServer): void {
108
108
  server.registerTool('gog_contacts_export', {
109
109
  description: 'Export contacts as vCard (.vcf). Provide a selector (resource name, email, or name), or use query / all to export multiple.',
110
110
  annotations: { readOnlyHint: true },
111
- inputSchema: {
111
+ inputSchema: z.object({
112
112
  selector: z.string().optional().describe('Contact resource name (people/...), email, or name'),
113
113
  query: z.string().optional().describe('Search query to export (max 30 results)'),
114
114
  all: z.boolean().optional().describe('Export all personal contacts'),
@@ -117,7 +117,7 @@ export function registerExtraContactsTools(server: McpServer): void {
117
117
  pageToken: pageTokenParam,
118
118
  page: pageAliasParam,
119
119
  account: accountParam,
120
- },
120
+ }),
121
121
  }, async ({ selector, query, all, out, max, pageToken, page, account }) => {
122
122
  const args = ['contacts', 'export'];
123
123
  if (selector) args.push(selector);
@@ -133,14 +133,14 @@ export function registerExtraContactsTools(server: McpServer): void {
133
133
  server.registerTool('gog_contacts_dedupe', {
134
134
  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.',
135
135
  annotations: { destructiveHint: true },
136
- inputSchema: {
136
+ inputSchema: z.object({
137
137
  match: z.string().optional().describe('Match fields, comma-separated from email,phone,name (default: email,phone)'),
138
138
  max: z.number().optional().describe('Max contacts to scan (0 = all). Mutually exclusive with resource — gog rejects passing both.'),
139
139
  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.'),
140
140
  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.'),
141
141
  failEmpty: z.boolean().optional().describe('Exit with an error (code 3) when no duplicates are found, instead of succeeding empty.'),
142
142
  account: accountParam,
143
- },
143
+ }),
144
144
  }, async ({ match, max, resource, apply, failEmpty, account }) => {
145
145
  const args = ['contacts', 'dedupe'];
146
146
  if (match) args.push(`--match=${match}`);
@@ -155,13 +155,13 @@ export function registerExtraContactsTools(server: McpServer): void {
155
155
  server.registerTool('gog_contacts_directory_list', {
156
156
  description: 'List people from the Google Workspace directory (domain shared contacts).',
157
157
  annotations: { readOnlyHint: true },
158
- inputSchema: {
158
+ inputSchema: z.object({
159
159
  max: z.number().optional().describe('Max results (default: 50)'),
160
160
  pageToken: pageTokenParam,
161
161
  page: pageAliasParam,
162
162
  all: z.boolean().optional().describe('Fetch all pages'),
163
163
  account: accountParam,
164
- },
164
+ }),
165
165
  }, async ({ max, pageToken, page, all, account }) => {
166
166
  const args = ['contacts', 'directory', 'list'];
167
167
  if (max !== undefined) args.push(`--max=${max}`);
@@ -174,13 +174,13 @@ export function registerExtraContactsTools(server: McpServer): void {
174
174
  server.registerTool('gog_contacts_other_list', {
175
175
  description: 'List "other contacts" — auto-collected addresses (e.g. people you have emailed) that are not in your saved contacts.',
176
176
  annotations: { readOnlyHint: true },
177
- inputSchema: {
177
+ inputSchema: z.object({
178
178
  max: z.number().optional().describe('Max results (default: 100)'),
179
179
  pageToken: pageTokenParam,
180
180
  page: pageAliasParam,
181
181
  all: z.boolean().optional().describe('Fetch all pages'),
182
182
  account: accountParam,
183
- },
183
+ }),
184
184
  }, async ({ max, pageToken, page, all, account }) => {
185
185
  const args = ['contacts', 'other', 'list'];
186
186
  if (max !== undefined) args.push(`--max=${max}`);
@@ -193,11 +193,11 @@ export function registerExtraContactsTools(server: McpServer): void {
193
193
  server.registerTool('gog_contacts_other_search', {
194
194
  description: 'Search "other contacts" — auto-collected addresses not in your saved contacts.',
195
195
  annotations: { readOnlyHint: true },
196
- inputSchema: {
196
+ inputSchema: z.object({
197
197
  query: z.string().describe('Search query'),
198
198
  max: z.number().optional().describe('Max results (default: 50)'),
199
199
  account: accountParam,
200
- },
200
+ }),
201
201
  }, async ({ query, max, account }) => {
202
202
  const args = ['contacts', 'other', 'search', query];
203
203
  if (max !== undefined) args.push(`--max=${max}`);
@@ -207,12 +207,12 @@ export function registerExtraContactsTools(server: McpServer): void {
207
207
  server.registerTool('gog_people_raw', {
208
208
  description: 'Dump the raw People API response as JSON (lossless; for scripting and LLM consumption).',
209
209
  annotations: { readOnlyHint: true },
210
- inputSchema: {
210
+ inputSchema: z.object({
211
211
  userId: z.string().describe('Person resource name (people/...) or email'),
212
212
  personFields: z.string().optional().describe('People API personFields mask (default: broad set)'),
213
213
  pretty: z.boolean().optional().describe('Pretty-print JSON (default: compact single-line)'),
214
214
  account: accountParam,
215
- },
215
+ }),
216
216
  }, async ({ userId, personFields, pretty, account }) => {
217
217
  const args = ['people', 'raw', userId];
218
218
  if (personFields) args.push(`--person-fields=${personFields}`);
package/tsconfig.json CHANGED
@@ -5,5 +5,5 @@
5
5
  "rootDir": "../"
6
6
  },
7
7
  "include": ["src/**/*", "../gogcli-mcp/src/**/*"],
8
- "exclude": ["node_modules", "dist", "../gogcli-mcp/src/worker.ts"]
8
+ "exclude": ["node_modules", "dist"]
9
9
  }