gogcli-mcp-drive 4.5.0 → 4.5.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +306 -60
- package/manifest.json +4 -4
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/tools/drive-extra.ts +78 -5
- package/tests/tools/drive-extra-gates.test.ts +154 -0
- package/tests/tools/drive-extra.test.ts +4 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
|
|
10
|
-
"version": "4.5.
|
|
10
|
+
"version": "4.5.2"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli (Drive)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
|
|
18
|
-
"version": "4.5.
|
|
18
|
+
"version": "4.5.2",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-drive",
|
|
3
3
|
"displayName": "gogcli (Drive)",
|
|
4
|
-
"version": "4.5.
|
|
4
|
+
"version": "4.5.2",
|
|
5
5
|
"description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/index.js
CHANGED
|
@@ -19790,7 +19790,7 @@ function date4(params) {
|
|
|
19790
19790
|
return _coercedDate(ZodDate, params);
|
|
19791
19791
|
}
|
|
19792
19792
|
|
|
19793
|
-
// ../../node_modules/@modelcontextprotocol/core/dist/auth-
|
|
19793
|
+
// ../../node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/dist/auth-CGP0BDVq.mjs
|
|
19794
19794
|
var LATEST_PROTOCOL_VERSION = "2025-11-25";
|
|
19795
19795
|
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
19796
19796
|
LATEST_PROTOCOL_VERSION,
|
|
@@ -20627,7 +20627,8 @@ var OAuthMetadataSchema = looseObject({
|
|
|
20627
20627
|
introspection_endpoint_auth_signing_alg_values_supported: array(string2()).optional(),
|
|
20628
20628
|
code_challenge_methods_supported: array(string2()).optional(),
|
|
20629
20629
|
client_id_metadata_document_supported: boolean2().optional(),
|
|
20630
|
-
authorization_response_iss_parameter_supported: boolean2().optional().catch(void 0)
|
|
20630
|
+
authorization_response_iss_parameter_supported: boolean2().optional().catch(void 0),
|
|
20631
|
+
dpop_signing_alg_values_supported: array(string2()).optional()
|
|
20631
20632
|
});
|
|
20632
20633
|
var OpenIdProviderMetadataSchema = looseObject({
|
|
20633
20634
|
issuer: string2(),
|
|
@@ -20728,7 +20729,7 @@ var OAuthTokenRevocationRequestSchema = object({
|
|
|
20728
20729
|
token_type_hint: string2().optional()
|
|
20729
20730
|
}).strip();
|
|
20730
20731
|
|
|
20731
|
-
// ../../node_modules/@modelcontextprotocol/server/dist/src-
|
|
20732
|
+
// ../../node_modules/@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs
|
|
20732
20733
|
var BRANDS = /* @__PURE__ */ Symbol.for("mcp.sdk.errorBrands");
|
|
20733
20734
|
function stampErrorBrands(instance, ctor) {
|
|
20734
20735
|
const brands = /* @__PURE__ */ new Set();
|
|
@@ -20842,8 +20843,18 @@ var SdkError = class extends Error {
|
|
|
20842
20843
|
if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SdkError.isInstance(value)`); for callbacks use `v => SdkError.isInstance(v)`");
|
|
20843
20844
|
return brandedHasInstance(this, value);
|
|
20844
20845
|
}
|
|
20845
|
-
|
|
20846
|
-
|
|
20846
|
+
/**
|
|
20847
|
+
* @param code - Stable string code identifying the failure ({@linkcode SdkErrorCode}).
|
|
20848
|
+
* @param message - Human-readable description.
|
|
20849
|
+
* @param data - Optional structured payload (for example the HTTP status carried by
|
|
20850
|
+
* {@linkcode SdkHttpError}). Opaque to the SDK: a `cause` key inside `data` is not
|
|
20851
|
+
* promoted to `Error.cause`.
|
|
20852
|
+
* @param options - Standard `ErrorOptions`, forwarded to `Error`. Pass the underlying
|
|
20853
|
+
* failure as `{ cause }` so it is reachable through the `Error.cause` chain that
|
|
20854
|
+
* loggers and error trackers walk.
|
|
20855
|
+
*/
|
|
20856
|
+
constructor(code, message, data, options) {
|
|
20857
|
+
super(message, options);
|
|
20847
20858
|
this.code = code;
|
|
20848
20859
|
this.data = data;
|
|
20849
20860
|
this.name = "SdkError";
|
|
@@ -20854,8 +20865,11 @@ var SdkHttpError = class extends SdkError {
|
|
|
20854
20865
|
static {
|
|
20855
20866
|
Object.defineProperty(this, "mcpBrand", { value: "mcp.SdkHttpError" });
|
|
20856
20867
|
}
|
|
20857
|
-
|
|
20858
|
-
|
|
20868
|
+
/**
|
|
20869
|
+
* @param options - Standard `ErrorOptions`, forwarded to `Error` (see {@linkcode SdkError}).
|
|
20870
|
+
*/
|
|
20871
|
+
constructor(code, message, data, options) {
|
|
20872
|
+
super(code, message, data, options);
|
|
20859
20873
|
this.name = "SdkHttpError";
|
|
20860
20874
|
}
|
|
20861
20875
|
get status() {
|
|
@@ -23806,7 +23820,7 @@ var INBOUND_VALIDATION_LADDER = [
|
|
|
23806
23820
|
evaluatedAt: "pre-dispatch",
|
|
23807
23821
|
codes: [HEADER_MISMATCH_ERROR_CODE],
|
|
23808
23822
|
conformance: ["http-header-validation"],
|
|
23809
|
-
rationale: "SEP-2243 standard `Mcp-Method` / `Mcp-Name` headers \u2014 presence, sentinel decoding, and `Mcp-Name` \u2194 body cross-check \u2014 are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before dispatch. The classifier\u2019s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge `era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half. Evaluated before the capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung is answered here before the dispatch rungs (5\u20136) are consulted."
|
|
23823
|
+
rationale: "SEP-2243 standard `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers \u2014 presence, sentinel decoding, and `Mcp-Name` \u2194 body cross-check \u2014 are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before dispatch. The spec requires `MCP-Protocol-Version` and `Mcp-Method` on every modern *request* POST (`Mcp-Name` only for the methods that mirror `params.name` / `params.uri` / `params.taskId`, see `MCP_NAME_HEADER_SOURCE`) and names them in that order, so a request missing several is answered by the earliest. Notification POSTs are exempt: the presence half runs on requests only, so a modern-enveloped notification is dispatched even with no standard headers at all. The classifier\u2019s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge `era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half \u2014 including the missing `MCP-Protocol-Version` cell, which cannot live on the edge rung without breaking body-primary classification. Evaluated before the capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung is answered here before the dispatch rungs (5\u20136) are consulted."
|
|
23810
23824
|
},
|
|
23811
23825
|
{
|
|
23812
23826
|
rung: "client-capabilities",
|
|
@@ -24607,7 +24621,7 @@ var Protocol = class {
|
|
|
24607
24621
|
return this._requestHandlers.get(method);
|
|
24608
24622
|
}
|
|
24609
24623
|
async _oncancel(notification) {
|
|
24610
|
-
if (
|
|
24624
|
+
if (notification.params.requestId === void 0) return;
|
|
24611
24625
|
this._requestHandlerAbortControllers.get(notification.params.requestId)?.abort(notification.params.reason);
|
|
24612
24626
|
}
|
|
24613
24627
|
_setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) {
|
|
@@ -25006,19 +25020,20 @@ var Protocol = class {
|
|
|
25006
25020
|
const cancel = (reason) => {
|
|
25007
25021
|
if (responseReceived) return;
|
|
25008
25022
|
this._progressHandlers.delete(messageId);
|
|
25009
|
-
if (requestAbort === void 0)
|
|
25010
|
-
|
|
25011
|
-
|
|
25012
|
-
|
|
25013
|
-
|
|
25014
|
-
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
|
|
25023
|
+
if (requestAbort === void 0) {
|
|
25024
|
+
if (request.method !== "initialize") this._transport?.send(this._envelopeOutbound({
|
|
25025
|
+
jsonrpc: "2.0",
|
|
25026
|
+
method: "notifications/cancelled",
|
|
25027
|
+
params: {
|
|
25028
|
+
requestId: messageId,
|
|
25029
|
+
reason: String(reason)
|
|
25030
|
+
}
|
|
25031
|
+
}), {
|
|
25032
|
+
relatedRequestId,
|
|
25033
|
+
resumptionToken,
|
|
25034
|
+
onresumptiontoken
|
|
25035
|
+
}).catch((error62) => this._onerror(/* @__PURE__ */ new Error(`Failed to send cancellation: ${error62}`)));
|
|
25036
|
+
} else requestAbort.abort();
|
|
25022
25037
|
reject(reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason)));
|
|
25023
25038
|
};
|
|
25024
25039
|
this._responseHandlers.set(messageId, (response) => {
|
|
@@ -25098,7 +25113,7 @@ var Protocol = class {
|
|
|
25098
25113
|
jsonrpc: "2.0",
|
|
25099
25114
|
...notification
|
|
25100
25115
|
});
|
|
25101
|
-
if ((this._options?.debouncedNotificationMethods ?? []).includes(notification.method) && !notification.params &&
|
|
25116
|
+
if ((this._options?.debouncedNotificationMethods ?? []).includes(notification.method) && !notification.params && options?.relatedRequestId === void 0) {
|
|
25102
25117
|
if (this._pendingDebouncedNotifications.has(notification.method)) return;
|
|
25103
25118
|
this._pendingDebouncedNotifications.add(notification.method);
|
|
25104
25119
|
Promise.resolve().then(() => {
|
|
@@ -32401,7 +32416,7 @@ var Ajv = import_ajv.Ajv;
|
|
|
32401
32416
|
// ../../node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs
|
|
32402
32417
|
import process2 from "node:process";
|
|
32403
32418
|
|
|
32404
|
-
// ../../node_modules/@modelcontextprotocol/server/dist/mcp-
|
|
32419
|
+
// ../../node_modules/@modelcontextprotocol/server/dist/mcp-Dw2OlZ1f.mjs
|
|
32405
32420
|
var COMPLETABLE_SYMBOL = /* @__PURE__ */ Symbol.for("mcp.completable");
|
|
32406
32421
|
function isCompletable(schema) {
|
|
32407
32422
|
return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
|
|
@@ -32594,6 +32609,9 @@ function notificationToServerEvent(method, uri) {
|
|
|
32594
32609
|
};
|
|
32595
32610
|
}
|
|
32596
32611
|
}
|
|
32612
|
+
function supportsScopeChallengeResolver(transport) {
|
|
32613
|
+
return typeof transport === "object" && transport !== null && "setScopeChallengeResolver" in transport && typeof transport.setScopeChallengeResolver === "function";
|
|
32614
|
+
}
|
|
32597
32615
|
var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
|
|
32598
32616
|
var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
|
|
32599
32617
|
function resolveLegacyShimOptions(options) {
|
|
@@ -33404,6 +33422,7 @@ var McpServer = class {
|
|
|
33404
33422
|
* ```
|
|
33405
33423
|
*/
|
|
33406
33424
|
async connect(transport) {
|
|
33425
|
+
if (supportsScopeChallengeResolver(transport)) transport.setScopeChallengeResolver((context) => this.resolveScopeChallenge(context));
|
|
33407
33426
|
return await this.server.connect(transport);
|
|
33408
33427
|
}
|
|
33409
33428
|
/**
|
|
@@ -33412,6 +33431,41 @@ var McpServer = class {
|
|
|
33412
33431
|
async close() {
|
|
33413
33432
|
await this.server.close();
|
|
33414
33433
|
}
|
|
33434
|
+
/** @internal */
|
|
33435
|
+
resolveScopeChallenge = (context) => {
|
|
33436
|
+
switch (context.request.method) {
|
|
33437
|
+
case "tools/call": {
|
|
33438
|
+
const toolName = context.request.params?.name;
|
|
33439
|
+
if (typeof toolName !== "string") return;
|
|
33440
|
+
const tool = this._registeredTools[toolName];
|
|
33441
|
+
if (tool === void 0 || !tool.enabled) return;
|
|
33442
|
+
return tool.scopeChallenge?.(context);
|
|
33443
|
+
}
|
|
33444
|
+
case "resources/read": {
|
|
33445
|
+
const resourceUri = context.request.params?.uri;
|
|
33446
|
+
if (typeof resourceUri !== "string") return;
|
|
33447
|
+
let uri;
|
|
33448
|
+
try {
|
|
33449
|
+
uri = new URL(resourceUri);
|
|
33450
|
+
} catch {
|
|
33451
|
+
return;
|
|
33452
|
+
}
|
|
33453
|
+
const resource = this._registeredResources[uri.toString()];
|
|
33454
|
+
if (resource !== void 0) return resource.enabled ? resource.scopeChallenge?.(context) : void 0;
|
|
33455
|
+
for (const template of Object.values(this._registeredResourceTemplates)) if (template.resourceTemplate.uriTemplate.match(uri.toString())) return template.enabled ? template.scopeChallenge?.(context) : void 0;
|
|
33456
|
+
return;
|
|
33457
|
+
}
|
|
33458
|
+
case "prompts/get": {
|
|
33459
|
+
const promptName = context.request.params?.name;
|
|
33460
|
+
if (typeof promptName !== "string") return;
|
|
33461
|
+
const prompt = this._registeredPrompts[promptName];
|
|
33462
|
+
if (prompt === void 0 || !prompt.enabled) return;
|
|
33463
|
+
return prompt.scopeChallenge?.(context);
|
|
33464
|
+
}
|
|
33465
|
+
default:
|
|
33466
|
+
return;
|
|
33467
|
+
}
|
|
33468
|
+
};
|
|
33415
33469
|
_toolHandlersInitialized = false;
|
|
33416
33470
|
setToolRequestHandlers() {
|
|
33417
33471
|
if (this._toolHandlersInitialized) return;
|
|
@@ -33578,7 +33632,10 @@ var McpServer = class {
|
|
|
33578
33632
|
}
|
|
33579
33633
|
for (const template of Object.values(this._registeredResourceTemplates)) {
|
|
33580
33634
|
const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
|
|
33581
|
-
if (variables)
|
|
33635
|
+
if (variables) {
|
|
33636
|
+
if (!template.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${template.resourceTemplate.uriTemplate} disabled`);
|
|
33637
|
+
return attachCacheHintFallback(await template.readCallback(uri, variables, ctx), template.cacheHint);
|
|
33638
|
+
}
|
|
33582
33639
|
}
|
|
33583
33640
|
throw new ResourceNotFoundError(request.params.uri);
|
|
33584
33641
|
});
|
|
@@ -33609,36 +33666,32 @@ var McpServer = class {
|
|
|
33609
33666
|
this._promptHandlersInitialized = true;
|
|
33610
33667
|
}
|
|
33611
33668
|
registerResource(name, uriOrTemplate, config2, readCallback) {
|
|
33612
|
-
const cacheHint = config2
|
|
33613
|
-
|
|
33614
|
-
if (cacheHint !== void 0) {
|
|
33615
|
-
assertValidCacheHint(cacheHint, `resource ${name}`);
|
|
33616
|
-
const rest = { ...config2 };
|
|
33617
|
-
delete rest.cacheHint;
|
|
33618
|
-
metadata = rest;
|
|
33619
|
-
}
|
|
33669
|
+
const { cacheHint, scopeChallenge, ...resourceMetadata } = config2;
|
|
33670
|
+
const metadata = resourceMetadata;
|
|
33671
|
+
if (cacheHint !== void 0) assertValidCacheHint(cacheHint, `resource ${name}`);
|
|
33620
33672
|
if (typeof uriOrTemplate === "string") {
|
|
33621
33673
|
if (this._registeredResources[uriOrTemplate]) throw new Error(`Resource ${uriOrTemplate} is already registered`);
|
|
33622
|
-
const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, readCallback);
|
|
33674
|
+
const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
|
|
33623
33675
|
if (cacheHint !== void 0) registeredResource.cacheHint = cacheHint;
|
|
33624
33676
|
this.setResourceRequestHandlers();
|
|
33625
33677
|
this.sendResourceListChanged();
|
|
33626
33678
|
return registeredResource;
|
|
33627
33679
|
} else {
|
|
33628
33680
|
if (this._registeredResourceTemplates[name]) throw new Error(`Resource template ${name} is already registered`);
|
|
33629
|
-
const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, readCallback);
|
|
33681
|
+
const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
|
|
33630
33682
|
if (cacheHint !== void 0) registeredResourceTemplate.cacheHint = cacheHint;
|
|
33631
33683
|
this.setResourceRequestHandlers();
|
|
33632
33684
|
this.sendResourceListChanged();
|
|
33633
33685
|
return registeredResourceTemplate;
|
|
33634
33686
|
}
|
|
33635
33687
|
}
|
|
33636
|
-
_createRegisteredResource(name, title, uri, metadata, readCallback) {
|
|
33688
|
+
_createRegisteredResource(name, title, uri, metadata, scopeChallenge, readCallback) {
|
|
33637
33689
|
const registeredResource = {
|
|
33638
33690
|
name,
|
|
33639
33691
|
title,
|
|
33640
33692
|
metadata,
|
|
33641
33693
|
readCallback,
|
|
33694
|
+
scopeChallenge,
|
|
33642
33695
|
enabled: true,
|
|
33643
33696
|
disable: () => registeredResource.update({ enabled: false }),
|
|
33644
33697
|
enable: () => registeredResource.update({ enabled: true }),
|
|
@@ -33652,6 +33705,7 @@ var McpServer = class {
|
|
|
33652
33705
|
if (updates.title !== void 0) registeredResource.title = updates.title;
|
|
33653
33706
|
if (updates.metadata !== void 0) registeredResource.metadata = updates.metadata;
|
|
33654
33707
|
if (updates.callback !== void 0) registeredResource.readCallback = updates.callback;
|
|
33708
|
+
if (updates.scopeChallenge !== void 0) registeredResource.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
|
|
33655
33709
|
if (updates.enabled !== void 0) registeredResource.enabled = updates.enabled;
|
|
33656
33710
|
this.sendResourceListChanged();
|
|
33657
33711
|
}
|
|
@@ -33659,12 +33713,13 @@ var McpServer = class {
|
|
|
33659
33713
|
this._registeredResources[uri] = registeredResource;
|
|
33660
33714
|
return registeredResource;
|
|
33661
33715
|
}
|
|
33662
|
-
_createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
|
|
33716
|
+
_createRegisteredResourceTemplate(name, title, template, metadata, scopeChallenge, readCallback) {
|
|
33663
33717
|
const registeredResourceTemplate = {
|
|
33664
33718
|
resourceTemplate: template,
|
|
33665
33719
|
title,
|
|
33666
33720
|
metadata,
|
|
33667
33721
|
readCallback,
|
|
33722
|
+
scopeChallenge,
|
|
33668
33723
|
enabled: true,
|
|
33669
33724
|
disable: () => registeredResourceTemplate.update({ enabled: false }),
|
|
33670
33725
|
enable: () => registeredResourceTemplate.update({ enabled: true }),
|
|
@@ -33678,6 +33733,7 @@ var McpServer = class {
|
|
|
33678
33733
|
if (updates.template !== void 0) registeredResourceTemplate.resourceTemplate = updates.template;
|
|
33679
33734
|
if (updates.metadata !== void 0) registeredResourceTemplate.metadata = updates.metadata;
|
|
33680
33735
|
if (updates.callback !== void 0) registeredResourceTemplate.readCallback = updates.callback;
|
|
33736
|
+
if (updates.scopeChallenge !== void 0) registeredResourceTemplate.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
|
|
33681
33737
|
if (updates.enabled !== void 0) registeredResourceTemplate.enabled = updates.enabled;
|
|
33682
33738
|
this.sendResourceListChanged();
|
|
33683
33739
|
}
|
|
@@ -33687,7 +33743,7 @@ var McpServer = class {
|
|
|
33687
33743
|
if (Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v))) this.setCompletionRequestHandler();
|
|
33688
33744
|
return registeredResourceTemplate;
|
|
33689
33745
|
}
|
|
33690
|
-
_createRegisteredPrompt(name, title, description, argsSchema, callback, icons, _meta) {
|
|
33746
|
+
_createRegisteredPrompt(name, title, description, argsSchema, callback, icons, scopeChallenge, _meta) {
|
|
33691
33747
|
let currentArgsSchema = argsSchema;
|
|
33692
33748
|
let currentCallback = callback;
|
|
33693
33749
|
const registeredPrompt = {
|
|
@@ -33695,6 +33751,7 @@ var McpServer = class {
|
|
|
33695
33751
|
description,
|
|
33696
33752
|
argsSchema,
|
|
33697
33753
|
icons,
|
|
33754
|
+
scopeChallenge,
|
|
33698
33755
|
_meta,
|
|
33699
33756
|
handler: createPromptHandler(name, argsSchema, callback),
|
|
33700
33757
|
enabled: true,
|
|
@@ -33709,6 +33766,7 @@ var McpServer = class {
|
|
|
33709
33766
|
if (updates.title !== void 0) registeredPrompt.title = updates.title;
|
|
33710
33767
|
if (updates.description !== void 0) registeredPrompt.description = updates.description;
|
|
33711
33768
|
if (updates.icons !== void 0) registeredPrompt.icons = updates.icons;
|
|
33769
|
+
if (updates.scopeChallenge !== void 0) registeredPrompt.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
|
|
33712
33770
|
if (updates._meta !== void 0) registeredPrompt._meta = updates._meta;
|
|
33713
33771
|
let needsHandlerRegen = false;
|
|
33714
33772
|
if (updates.argsSchema !== void 0) {
|
|
@@ -33736,7 +33794,7 @@ var McpServer = class {
|
|
|
33736
33794
|
}
|
|
33737
33795
|
return registeredPrompt;
|
|
33738
33796
|
}
|
|
33739
|
-
_createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, _meta, handler) {
|
|
33797
|
+
_createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, scopeChallenge, _meta, handler) {
|
|
33740
33798
|
validateAndWarnToolName(name);
|
|
33741
33799
|
if (inputSchema !== void 0) try {
|
|
33742
33800
|
const json2 = standardSchemaToJsonSchema(inputSchema, "input");
|
|
@@ -33755,6 +33813,7 @@ var McpServer = class {
|
|
|
33755
33813
|
annotations,
|
|
33756
33814
|
icons,
|
|
33757
33815
|
execution,
|
|
33816
|
+
scopeChallenge,
|
|
33758
33817
|
_meta,
|
|
33759
33818
|
handler,
|
|
33760
33819
|
executor: createToolExecutor(inputSchema, handler),
|
|
@@ -33793,6 +33852,7 @@ var McpServer = class {
|
|
|
33793
33852
|
}
|
|
33794
33853
|
if (updates.annotations !== void 0) registeredTool.annotations = updates.annotations;
|
|
33795
33854
|
if (updates.icons !== void 0) registeredTool.icons = updates.icons;
|
|
33855
|
+
if (updates.scopeChallenge !== void 0) registeredTool.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
|
|
33796
33856
|
if (updates._meta !== void 0) registeredTool._meta = updates._meta;
|
|
33797
33857
|
if (updates.enabled !== void 0) registeredTool.enabled = updates.enabled;
|
|
33798
33858
|
this.sendToolListChanged();
|
|
@@ -33805,13 +33865,13 @@ var McpServer = class {
|
|
|
33805
33865
|
}
|
|
33806
33866
|
registerTool(name, config2, cb) {
|
|
33807
33867
|
if (this._registeredTools[name]) throw new Error(`Tool ${name} is already registered`);
|
|
33808
|
-
const { title, description, inputSchema, outputSchema, annotations, icons, _meta } = config2;
|
|
33809
|
-
return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, _meta, cb);
|
|
33868
|
+
const { title, description, inputSchema, outputSchema, annotations, icons, scopeChallenge, _meta } = config2;
|
|
33869
|
+
return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, scopeChallenge, _meta, cb);
|
|
33810
33870
|
}
|
|
33811
33871
|
registerPrompt(name, config2, cb) {
|
|
33812
33872
|
if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
|
|
33813
|
-
const { title, description, argsSchema, icons, _meta } = config2;
|
|
33814
|
-
const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, _meta);
|
|
33873
|
+
const { title, description, argsSchema, icons, scopeChallenge, _meta } = config2;
|
|
33874
|
+
const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, scopeChallenge, _meta);
|
|
33815
33875
|
this.setPromptRequestHandlers();
|
|
33816
33876
|
this.sendPromptListChanged();
|
|
33817
33877
|
return registeredPrompt;
|
|
@@ -33923,6 +33983,7 @@ function unwrapOptionalSchema(schema) {
|
|
|
33923
33983
|
}
|
|
33924
33984
|
|
|
33925
33985
|
// ../../node_modules/@modelcontextprotocol/server/dist/stdio.mjs
|
|
33986
|
+
var swallowsErrorsAfterClose = /* @__PURE__ */ Symbol("swallowsErrorsAfterClose");
|
|
33926
33987
|
var StdioServerTransport = class {
|
|
33927
33988
|
_readBuffer;
|
|
33928
33989
|
_started = false;
|
|
@@ -33949,18 +34010,27 @@ var StdioServerTransport = class {
|
|
|
33949
34010
|
this.onerror?.(error62);
|
|
33950
34011
|
};
|
|
33951
34012
|
_onstdouterror = (error62) => {
|
|
34013
|
+
if (this._closed) return;
|
|
33952
34014
|
this.onerror?.(error62);
|
|
33953
34015
|
this.close().catch(() => {
|
|
33954
34016
|
});
|
|
33955
34017
|
};
|
|
34018
|
+
_onstdinclose = () => {
|
|
34019
|
+
this.close().catch(() => {
|
|
34020
|
+
});
|
|
34021
|
+
};
|
|
33956
34022
|
/**
|
|
33957
34023
|
* Starts listening for messages on `stdin`.
|
|
33958
34024
|
*/
|
|
33959
34025
|
async start() {
|
|
33960
34026
|
if (this._started) throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
|
|
33961
34027
|
this._started = true;
|
|
34028
|
+
for (const listener of this._stdout.listeners("error")) if (listener[swallowsErrorsAfterClose]) this._stdout.off("error", listener);
|
|
34029
|
+
if (this._stdin.readableEnded || this._stdin.destroyed) setImmediate(this._onstdinclose);
|
|
33962
34030
|
this._stdin.on("data", this._ondata);
|
|
33963
34031
|
this._stdin.on("error", this._onerror);
|
|
34032
|
+
this._stdin.on("end", this._onstdinclose);
|
|
34033
|
+
this._stdin.on("close", this._onstdinclose);
|
|
33964
34034
|
this._stdout.on("error", this._onstdouterror);
|
|
33965
34035
|
}
|
|
33966
34036
|
processReadBuffer() {
|
|
@@ -33977,7 +34047,9 @@ var StdioServerTransport = class {
|
|
|
33977
34047
|
this._closed = true;
|
|
33978
34048
|
this._stdin.off("data", this._ondata);
|
|
33979
34049
|
this._stdin.off("error", this._onerror);
|
|
33980
|
-
this.
|
|
34050
|
+
this._stdin.off("end", this._onstdinclose);
|
|
34051
|
+
this._stdin.off("close", this._onstdinclose);
|
|
34052
|
+
this._onstdouterror[swallowsErrorsAfterClose] = true;
|
|
33981
34053
|
if (this._stdin.listenerCount("data") === 0) this._stdin.pause();
|
|
33982
34054
|
this._readBuffer.clear();
|
|
33983
34055
|
this.onclose?.();
|
|
@@ -34254,6 +34326,7 @@ function serveStdio(factory, options = {}) {
|
|
|
34254
34326
|
return;
|
|
34255
34327
|
}
|
|
34256
34328
|
if (state.era === "modern" && await tryServeListen(message)) return;
|
|
34329
|
+
if (isTornDown()) return;
|
|
34257
34330
|
state.instance.channel.deliver(message);
|
|
34258
34331
|
return;
|
|
34259
34332
|
}
|
|
@@ -34320,6 +34393,7 @@ function serveStdio(factory, options = {}) {
|
|
|
34320
34393
|
};
|
|
34321
34394
|
}
|
|
34322
34395
|
if (await tryServeListen(message)) return;
|
|
34396
|
+
if (isTornDown()) return;
|
|
34323
34397
|
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
34324
34398
|
return;
|
|
34325
34399
|
case "legacy": {
|
|
@@ -34406,6 +34480,9 @@ function toError(value) {
|
|
|
34406
34480
|
return value instanceof Error ? value : new Error(String(value));
|
|
34407
34481
|
}
|
|
34408
34482
|
|
|
34483
|
+
// ../../node_modules/@modelcontextprotocol/server/dist/index.mjs
|
|
34484
|
+
var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
|
|
34485
|
+
|
|
34409
34486
|
// ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
34410
34487
|
var McpToolError = class extends Error {
|
|
34411
34488
|
/** Actionable remediation text, when one applies. */
|
|
@@ -35928,6 +36005,7 @@ async function diagnose(err) {
|
|
|
35928
36005
|
const isAuthError = !isTransientError && AUTH_ERROR_PATTERN.test(errText);
|
|
35929
36006
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
35930
36007
|
const hint = isInvalidGrant ? INVALID_GRANT_HINT : isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
|
|
36008
|
+
if (!isAuthError && !isInvalidGrant) return errorResult(`${errText}${hint}`);
|
|
35931
36009
|
try {
|
|
35932
36010
|
const accounts = formatAccountList(await run(["auth", "list"]));
|
|
35933
36011
|
return errorResult(`${errText}
|
|
@@ -36023,10 +36101,51 @@ function confirmSpentStore() {
|
|
|
36023
36101
|
}
|
|
36024
36102
|
|
|
36025
36103
|
// ../gogcli-mcp/src/dispatch-confirmation.ts
|
|
36104
|
+
var BODY_PREVIEW_MAX = 2048;
|
|
36105
|
+
function bodyPreview(text) {
|
|
36106
|
+
if (!text) return void 0;
|
|
36107
|
+
if (text.length <= BODY_PREVIEW_MAX) return text;
|
|
36108
|
+
return `${text.slice(0, BODY_PREVIEW_MAX)}\u2026 [${text.length - BODY_PREVIEW_MAX} more characters not shown]`;
|
|
36109
|
+
}
|
|
36026
36110
|
var CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
|
|
36027
36111
|
var CONFIRM_FALLBACK_DESCRIPTION = ' If the client cannot show that prompt (no MCP elicitation, e.g. claude.ai), a two-step flow applies instead: call WITHOUT confirmToken and nothing is sent or changed \u2014 the result has status "confirmation-required", the full preview and a confirmToken. Follow its instruction (by default: show the preview to the user verbatim and only after they explicitly approve it in chat, call again with the SAME arguments plus confirmToken). The tool re-reads what it would act on and refuses (DRAFT_CHANGED, with a fresh preview and token) if it changed; TOKEN_EXPIRED / TOKEN_REUSED / TOKEN_INVALID also do nothing. A server set to MCP_CONFIRM_MODE=refuse refuses instead.';
|
|
36112
|
+
function refusedInRun(what, via, does, instead) {
|
|
36113
|
+
return `${what} ${does} and is not available through ${via}. ${instead}`;
|
|
36114
|
+
}
|
|
36028
36115
|
function gatedElsewhere(what, via, does, tool) {
|
|
36029
|
-
return
|
|
36116
|
+
return refusedInRun(what, via, does, `Use ${tool}, which asks the user to confirm.`);
|
|
36117
|
+
}
|
|
36118
|
+
function hasCommandWord(args, words) {
|
|
36119
|
+
return args.find((a) => words.has(a.toLowerCase()));
|
|
36120
|
+
}
|
|
36121
|
+
var COMMENT_ADD_WORDS = /* @__PURE__ */ new Set(["create", "add", "new"]);
|
|
36122
|
+
var COMMENT_REPLY_WORDS = /* @__PURE__ */ new Set(["reply", "respond"]);
|
|
36123
|
+
function hasTrueFlag(args, flag) {
|
|
36124
|
+
const name = `--${flag}`;
|
|
36125
|
+
return args.some((a) => {
|
|
36126
|
+
const lower = a.toLowerCase();
|
|
36127
|
+
if (lower === name) return true;
|
|
36128
|
+
return lower.startsWith(`${name}=`) && lower.slice(name.length + 1) !== "false";
|
|
36129
|
+
});
|
|
36130
|
+
}
|
|
36131
|
+
function vetCommentsRun(service, subcommand, args) {
|
|
36132
|
+
if (subcommand.toLowerCase() !== "comments") return void 0;
|
|
36133
|
+
const reply = hasCommandWord(args, COMMENT_REPLY_WORDS);
|
|
36134
|
+
if (reply) {
|
|
36135
|
+
return gatedElsewhere(
|
|
36136
|
+
`gog ${service} comments ${reply.toLowerCase()}`,
|
|
36137
|
+
`gog_${service}_run`,
|
|
36138
|
+
"notifies the comment thread",
|
|
36139
|
+
`gog_${service}_comments_reply`
|
|
36140
|
+
);
|
|
36141
|
+
}
|
|
36142
|
+
const add = hasCommandWord(args, COMMENT_ADD_WORDS);
|
|
36143
|
+
return add ? gatedElsewhere(
|
|
36144
|
+
`gog ${service} comments ${add.toLowerCase()}`,
|
|
36145
|
+
`gog_${service}_run`,
|
|
36146
|
+
"notifies the file's owner and anyone it mentions",
|
|
36147
|
+
`gog_${service}_comments_add`
|
|
36148
|
+
) : void 0;
|
|
36030
36149
|
}
|
|
36031
36150
|
async function requireDispatchConfirmation(ctx, options) {
|
|
36032
36151
|
const { action, fallback } = options;
|
|
@@ -36188,6 +36307,27 @@ function authToolsFor(defaultServices) {
|
|
|
36188
36307
|
}
|
|
36189
36308
|
|
|
36190
36309
|
// ../gogcli-mcp/src/tools/calendar.ts
|
|
36310
|
+
var ASKS = (tool) => `Use ${tool}, which asks the user to confirm.`;
|
|
36311
|
+
var CALENDAR_REFUSED = {
|
|
36312
|
+
create: { does: "can change what guests see", instead: ASKS("gog_calendar_create") },
|
|
36313
|
+
add: { does: "can change what guests see", instead: ASKS("gog_calendar_create") },
|
|
36314
|
+
new: { does: "can change what guests see", instead: ASKS("gog_calendar_create") },
|
|
36315
|
+
update: { does: "can change what guests see", instead: ASKS("gog_calendar_update") },
|
|
36316
|
+
edit: { does: "can change what guests see", instead: ASKS("gog_calendar_update") },
|
|
36317
|
+
set: { does: "can change what guests see", instead: ASKS("gog_calendar_update") },
|
|
36318
|
+
respond: { does: "can change what guests see", instead: ASKS("gog_calendar_respond") },
|
|
36319
|
+
rsvp: { does: "can change what guests see", instead: ASKS("gog_calendar_respond") },
|
|
36320
|
+
reply: { does: "can change what guests see", instead: ASKS("gog_calendar_respond") },
|
|
36321
|
+
move: { does: "moves an event off guests' calendars (and can email them)", instead: ASKS("gog_calendar_move") },
|
|
36322
|
+
transfer: { does: "moves an event off guests' calendars (and can email them)", instead: ASKS("gog_calendar_move") },
|
|
36323
|
+
delete: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36324
|
+
del: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36325
|
+
rm: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36326
|
+
remove: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36327
|
+
"delete-calendar": { does: "deletes a calendar and every event on it", instead: ASKS("gog_calendar_delete_calendar") },
|
|
36328
|
+
"out-of-office": { does: "auto-declines invitations and notifies their organizers", instead: ASKS("gog_calendar_out_of_office") },
|
|
36329
|
+
ooo: { does: "auto-declines invitations and notifies their organizers", instead: ASKS("gog_calendar_out_of_office") }
|
|
36330
|
+
};
|
|
36191
36331
|
var reminderParams = {
|
|
36192
36332
|
reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
|
|
36193
36333
|
`Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
|
|
@@ -36253,8 +36393,25 @@ function inlineFileArg(flag, attachment, opts = {}) {
|
|
|
36253
36393
|
}
|
|
36254
36394
|
|
|
36255
36395
|
// ../gogcli-mcp/src/tools/drive.ts
|
|
36256
|
-
|
|
36257
|
-
|
|
36396
|
+
var DRIVE_DELETE_WORDS = /* @__PURE__ */ new Set(["delete", "rm", "del"]);
|
|
36397
|
+
function vetDriveRun(subcommand, args) {
|
|
36398
|
+
const sub = subcommand.toLowerCase();
|
|
36399
|
+
if (sub === "share") return gatedElsewhere("gog drive share", "gog_drive_run", "grants access to a file", "gog_drive_share");
|
|
36400
|
+
if (DRIVE_DELETE_WORDS.has(sub) && hasTrueFlag(args, "permanent")) {
|
|
36401
|
+
return gatedElsewhere(`gog drive ${sub} --permanent`, "gog_drive_run", "deletes a file for good, bypassing the trash", "gog_drive_delete");
|
|
36402
|
+
}
|
|
36403
|
+
if (sub === "bulk") {
|
|
36404
|
+
return gatedElsewhere(
|
|
36405
|
+
"gog drive bulk",
|
|
36406
|
+
"gog_drive_run",
|
|
36407
|
+
"rewrites sharing permissions across every matching file in a tree",
|
|
36408
|
+
"gog_drive_share (one file at a time)"
|
|
36409
|
+
);
|
|
36410
|
+
}
|
|
36411
|
+
if (sub === "unshare") {
|
|
36412
|
+
return refusedInRun("gog drive unshare", "gog_drive_run", "removes someone's access to a file", "Use gog_drive_unshare.");
|
|
36413
|
+
}
|
|
36414
|
+
return vetCommentsRun("drive", sub, args);
|
|
36258
36415
|
}
|
|
36259
36416
|
var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
|
|
36260
36417
|
function shareTargetMeta(raw) {
|
|
@@ -36265,6 +36422,26 @@ function shareTargetMeta(raw) {
|
|
|
36265
36422
|
return {};
|
|
36266
36423
|
}
|
|
36267
36424
|
}
|
|
36425
|
+
function commentSnapshot(raw) {
|
|
36426
|
+
let comment;
|
|
36427
|
+
try {
|
|
36428
|
+
const parsed = JSON.parse(raw);
|
|
36429
|
+
comment = parsed?.comment ?? parsed ?? void 0;
|
|
36430
|
+
} catch {
|
|
36431
|
+
comment = void 0;
|
|
36432
|
+
}
|
|
36433
|
+
const name = typeof comment?.author?.displayName === "string" ? comment.author.displayName : void 0;
|
|
36434
|
+
const email3 = typeof comment?.author?.emailAddress === "string" ? comment.author.emailAddress : void 0;
|
|
36435
|
+
const author = name && email3 ? `${name} <${email3}>` : name ?? email3;
|
|
36436
|
+
return {
|
|
36437
|
+
...author ? { author } : {},
|
|
36438
|
+
...typeof comment?.content === "string" ? { content: comment.content } : {},
|
|
36439
|
+
...typeof comment?.resolved === "boolean" ? { resolved: comment.resolved } : {}
|
|
36440
|
+
};
|
|
36441
|
+
}
|
|
36442
|
+
function commentMentions(text) {
|
|
36443
|
+
return [...new Set(text.match(/(?<=[+@])[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g) ?? [])];
|
|
36444
|
+
}
|
|
36268
36445
|
function fileMeta(raw) {
|
|
36269
36446
|
const parsed = JSON.parse(raw);
|
|
36270
36447
|
const f = parsed.file ?? parsed;
|
|
@@ -36373,16 +36550,36 @@ function registerDriveTools(server) {
|
|
|
36373
36550
|
return runOrDiagnose(["drive", "move", pos(fileId), `--parent=${parentId}`], { account });
|
|
36374
36551
|
});
|
|
36375
36552
|
server.registerTool("gog_drive_delete", {
|
|
36376
|
-
description: "Move a Google Drive file to trash, or permanently delete it with permanent=true (irreversible).",
|
|
36553
|
+
description: "Move a Google Drive file to trash, or permanently delete it with permanent=true (irreversible). A permanent delete reads the file and asks the MCP host to show the user a confirmation prompt naming it first; nothing is deleted unless they accept. Moving to trash is recoverable and does not ask." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36377
36554
|
annotations: { destructiveHint: true },
|
|
36378
36555
|
inputSchema: external_exports.object({
|
|
36379
36556
|
fileId: external_exports.string().describe("File ID to delete"),
|
|
36380
|
-
permanent: external_exports.boolean().optional().describe("Permanently delete instead of moving to trash (irreversible)"),
|
|
36381
|
-
account: accountParam
|
|
36557
|
+
permanent: external_exports.boolean().optional().describe("Permanently delete instead of moving to trash (irreversible; asks the user first)"),
|
|
36558
|
+
account: accountParam,
|
|
36559
|
+
confirmToken: confirmTokenParam
|
|
36382
36560
|
})
|
|
36383
|
-
}, async ({ fileId, permanent, account }) => {
|
|
36561
|
+
}, async ({ fileId, permanent, account, confirmToken }, ctx) => {
|
|
36384
36562
|
const args = ["drive", "delete", pos(fileId)];
|
|
36385
|
-
if (permanent)
|
|
36563
|
+
if (permanent) {
|
|
36564
|
+
const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
|
|
36565
|
+
if (got.isError) return got;
|
|
36566
|
+
const target = { file: { id: fileId, ...shareTargetMeta(resultText(got)) }, permanent: true };
|
|
36567
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36568
|
+
action: "drive.delete-permanent",
|
|
36569
|
+
message: "Review and confirm PERMANENTLY deleting this file \u2014 it skips the trash and cannot be recovered:",
|
|
36570
|
+
confirmationLabel: "Confirm that this file should be permanently deleted now.",
|
|
36571
|
+
details: target,
|
|
36572
|
+
unsupportedNote: "Move it to the trash instead (permanent=false); the user can empty the trash from Google Drive.",
|
|
36573
|
+
fallback: {
|
|
36574
|
+
tool: "gog_drive_delete",
|
|
36575
|
+
account,
|
|
36576
|
+
confirmToken,
|
|
36577
|
+
subject: () => ({ target: fileId, payload: target, preview: target })
|
|
36578
|
+
}
|
|
36579
|
+
});
|
|
36580
|
+
if (confirmation) return confirmation;
|
|
36581
|
+
args.push("--permanent");
|
|
36582
|
+
}
|
|
36386
36583
|
args.push("--force");
|
|
36387
36584
|
return runOrDiagnose(args, { account });
|
|
36388
36585
|
});
|
|
@@ -36572,7 +36769,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
36572
36769
|
);
|
|
36573
36770
|
|
|
36574
36771
|
// ../gogcli-mcp/src/server.ts
|
|
36575
|
-
var VERSION = true ? "4.5.
|
|
36772
|
+
var VERSION = true ? "4.5.2" : "0.0.0";
|
|
36576
36773
|
|
|
36577
36774
|
// ../gogcli-mcp/src/bootstrap-auth.ts
|
|
36578
36775
|
import { join as join5 } from "node:path";
|
|
@@ -36836,14 +37033,37 @@ function registerExtraDriveTools(server) {
|
|
|
36836
37033
|
return runOrDiagnose(["drive", "comments", "get", pos(fileId), pos(commentId)], { account });
|
|
36837
37034
|
});
|
|
36838
37035
|
server.registerTool("gog_drive_comments_add", {
|
|
36839
|
-
description: "Add a new comment to a Drive file.",
|
|
37036
|
+
description: "Add a new comment to a Drive file. Drive notifies the file's owner and everyone the text +mentions, so this reads the file and asks the MCP host to show the user a confirmation prompt with the file, the text and who is mentioned first; nothing is posted unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36840
37037
|
annotations: { destructiveHint: false },
|
|
36841
37038
|
inputSchema: external_exports.object({
|
|
36842
37039
|
fileId: external_exports.string().describe("File ID"),
|
|
36843
37040
|
content: external_exports.string().describe("Comment text"),
|
|
36844
|
-
account: accountParam
|
|
37041
|
+
account: accountParam,
|
|
37042
|
+
confirmToken: confirmTokenParam
|
|
36845
37043
|
})
|
|
36846
|
-
}, async ({ fileId, content, account }) => {
|
|
37044
|
+
}, async ({ fileId, content, account, confirmToken }, ctx) => {
|
|
37045
|
+
const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
|
|
37046
|
+
if (got.isError) return got;
|
|
37047
|
+
const { name } = shareTargetMeta(resultText(got));
|
|
37048
|
+
const file2 = { id: fileId, ...name !== void 0 ? { name } : {} };
|
|
37049
|
+
const mentions = commentMentions(content);
|
|
37050
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37051
|
+
action: "drive.comment-add",
|
|
37052
|
+
message: "Review and confirm this comment \u2014 the file's owner and anyone mentioned are notified:",
|
|
37053
|
+
confirmationLabel: "Confirm that this comment should be posted now.",
|
|
37054
|
+
details: { file: file2, textPreview: bodyPreview(content), mentions },
|
|
37055
|
+
unsupportedNote: "Ask the user to comment from Google Drive.",
|
|
37056
|
+
fallback: {
|
|
37057
|
+
tool: "gog_drive_comments_add",
|
|
37058
|
+
account,
|
|
37059
|
+
confirmToken,
|
|
37060
|
+
subject: () => {
|
|
37061
|
+
const view = { file: file2, text: content, mentions };
|
|
37062
|
+
return { target: fileId, payload: view, preview: view };
|
|
37063
|
+
}
|
|
37064
|
+
}
|
|
37065
|
+
});
|
|
37066
|
+
if (confirmation) return confirmation;
|
|
36847
37067
|
return runOrDiagnose(["drive", "comments", "create", pos(fileId), pos(content)], { account });
|
|
36848
37068
|
});
|
|
36849
37069
|
server.registerTool("gog_drive_comments_update", {
|
|
@@ -36870,18 +37090,44 @@ function registerExtraDriveTools(server) {
|
|
|
36870
37090
|
return runOrDiagnose(["drive", "comments", "delete", pos(fileId), pos(commentId), "--force"], { account });
|
|
36871
37091
|
});
|
|
36872
37092
|
server.registerTool("gog_drive_comments_reply", {
|
|
36873
|
-
description: 'Reply to an existing comment on a Drive file. Pass `action: "resolve"` or `"reopen"` to atomically flip the parent comment\'s resolved state via the Drive API\'s Reply.action field \u2014 avoids the older workaround of deleting the comment (which destroys review-thread context).',
|
|
37093
|
+
description: 'Reply to an existing comment on a Drive file. Pass `action: "resolve"` or `"reopen"` to atomically flip the parent comment\'s resolved state via the Drive API\'s Reply.action field \u2014 avoids the older workaround of deleting the comment (which destroys review-thread context). Everyone on the thread and anyone the reply +mentions is notified, so this reads the file and the comment and asks the MCP host to show the user a confirmation prompt with both and the reply first; nothing is posted unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36874
37094
|
annotations: { destructiveHint: false },
|
|
36875
37095
|
inputSchema: external_exports.object({
|
|
36876
37096
|
fileId: external_exports.string().describe("File ID"),
|
|
36877
37097
|
commentId: external_exports.string().describe("Comment ID to reply to"),
|
|
36878
37098
|
content: external_exports.string().describe("Reply text"),
|
|
36879
37099
|
action: external_exports.enum(["resolve", "reopen"]).optional().describe("Optional action on the parent comment alongside the reply"),
|
|
36880
|
-
account: accountParam
|
|
37100
|
+
account: accountParam,
|
|
37101
|
+
confirmToken: confirmTokenParam
|
|
36881
37102
|
})
|
|
36882
|
-
}, async ({ fileId, commentId, content, action, account }) => {
|
|
37103
|
+
}, async ({ fileId, commentId, content, action, account, confirmToken }, ctx) => {
|
|
36883
37104
|
const args = ["drive", "comments", "reply", pos(fileId), pos(commentId), pos(content)];
|
|
36884
37105
|
if (action) args.push(`--action=${action}`);
|
|
37106
|
+
const got = await runOrDiagnose(["drive", "get", pos(fileId)], { account });
|
|
37107
|
+
if (got.isError) return got;
|
|
37108
|
+
const thread = await runOrDiagnose(["drive", "comments", "get", pos(fileId), pos(commentId)], { account });
|
|
37109
|
+
if (thread.isError) return thread;
|
|
37110
|
+
const { name } = shareTargetMeta(resultText(got));
|
|
37111
|
+
const file2 = { id: fileId, ...name !== void 0 ? { name } : {} };
|
|
37112
|
+
const replyingTo = commentSnapshot(resultText(thread));
|
|
37113
|
+
const mentions = commentMentions(content);
|
|
37114
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37115
|
+
action: "drive.comment-reply",
|
|
37116
|
+
message: "Review and confirm this reply \u2014 everyone on the thread and anyone mentioned is notified:",
|
|
37117
|
+
confirmationLabel: "Confirm that this reply should be posted now.",
|
|
37118
|
+
details: { file: file2, replyingTo, textPreview: bodyPreview(content), mentions, ...action ? { action } : {} },
|
|
37119
|
+
unsupportedNote: "Ask the user to reply from Google Drive.",
|
|
37120
|
+
fallback: {
|
|
37121
|
+
tool: "gog_drive_comments_reply",
|
|
37122
|
+
account,
|
|
37123
|
+
confirmToken,
|
|
37124
|
+
subject: () => {
|
|
37125
|
+
const view = { file: file2, replyingTo, text: content, mentions, ...action ? { action } : {} };
|
|
37126
|
+
return { target: `${fileId}/${commentId}`, payload: view, preview: view };
|
|
37127
|
+
}
|
|
37128
|
+
}
|
|
37129
|
+
});
|
|
37130
|
+
if (confirmation) return confirmation;
|
|
36885
37131
|
return runOrDiagnose(args, { account });
|
|
36886
37132
|
});
|
|
36887
37133
|
server.registerTool("gog_drive_comments_resolve", {
|
|
@@ -37206,7 +37452,7 @@ await runMcp({
|
|
|
37206
37452
|
});
|
|
37207
37453
|
/*! Bundled license information:
|
|
37208
37454
|
|
|
37209
|
-
@modelcontextprotocol/server/dist/src-
|
|
37455
|
+
@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs:
|
|
37210
37456
|
(*!
|
|
37211
37457
|
* content-type
|
|
37212
37458
|
* Copyright(c) 2015 Douglas Christopher Wilson
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-drive",
|
|
5
5
|
"display_name": "gogcli (Drive)",
|
|
6
|
-
"version": "4.5.
|
|
6
|
+
"version": "4.5.2",
|
|
7
7
|
"description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"name": "gog_drive_delete",
|
|
133
|
-
"description": "Delete a file (trash by default, or permanent=true for irreversible delete)"
|
|
133
|
+
"description": "Delete a file (trash by default, or permanent=true for irreversible delete) (a permanent delete asks the user to confirm)"
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"name": "gog_drive_share",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
},
|
|
183
183
|
{
|
|
184
184
|
"name": "gog_drive_comments_add",
|
|
185
|
-
"description": "Add a new comment"
|
|
185
|
+
"description": "Add a new comment (asks the user to confirm)"
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
"name": "gog_drive_comments_update",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
},
|
|
195
195
|
{
|
|
196
196
|
"name": "gog_drive_comments_reply",
|
|
197
|
-
"description": "Reply to an existing comment (optionally resolve/reopen the parent atomically via action)"
|
|
197
|
+
"description": "Reply to an existing comment (optionally resolve/reopen the parent atomically via action) (asks the user to confirm)"
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
"name": "gog_drive_comments_resolve",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-drive",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-drive",
|
|
5
5
|
"description": "Extended Google Drive MCP server via gogcli — auth + full Drive support (upload/download/permissions/comments/shared drives)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@chrischall/mcp-utils": "^2.6.0",
|
|
28
|
-
"@modelcontextprotocol/server": "^2.
|
|
28
|
+
"@modelcontextprotocol/server": "^2.1.0",
|
|
29
29
|
"zod": "^4.6.1"
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp-drive"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.5.
|
|
10
|
+
"version": "4.5.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-drive",
|
|
15
|
-
"version": "4.5.
|
|
15
|
+
"version": "4.5.2",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/tools/drive-extra.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
accountParam,
|
|
5
|
+
runOrDiagnose,
|
|
6
|
+
paginationParams,
|
|
7
|
+
pushPaginationFlags,
|
|
8
|
+
pageTokenParam,
|
|
9
|
+
pageAliasParam,
|
|
10
|
+
resolvePageToken,
|
|
11
|
+
inlineFileArg,
|
|
12
|
+
pos,
|
|
13
|
+
confinePath,
|
|
14
|
+
bodyPreview,
|
|
15
|
+
commentMentions,
|
|
16
|
+
commentSnapshot,
|
|
17
|
+
CONFIRM_FALLBACK_DESCRIPTION,
|
|
18
|
+
confirmTokenParam,
|
|
19
|
+
requireDispatchConfirmation,
|
|
20
|
+
resultText,
|
|
21
|
+
shareTargetMeta,
|
|
22
|
+
} from '../../../gogcli-mcp/src/lib.js';
|
|
4
23
|
import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
|
|
5
24
|
|
|
6
25
|
export function registerExtraDriveTools(server: McpServer): void {
|
|
@@ -222,14 +241,39 @@ export function registerExtraDriveTools(server: McpServer): void {
|
|
|
222
241
|
});
|
|
223
242
|
|
|
224
243
|
server.registerTool('gog_drive_comments_add', {
|
|
225
|
-
description: 'Add a new comment to a Drive file.',
|
|
244
|
+
description: 'Add a new comment to a Drive file. Drive notifies the file\'s owner and everyone the text +mentions, so '
|
|
245
|
+
+ 'this reads the file and asks the MCP host to show the user a confirmation prompt with the file, the text and who '
|
|
246
|
+
+ 'is mentioned first; nothing is posted unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
226
247
|
annotations: { destructiveHint: false },
|
|
227
248
|
inputSchema: z.object({
|
|
228
249
|
fileId: z.string().describe('File ID'),
|
|
229
250
|
content: z.string().describe('Comment text'),
|
|
230
251
|
account: accountParam,
|
|
252
|
+
confirmToken: confirmTokenParam,
|
|
231
253
|
}),
|
|
232
|
-
}, async ({ fileId, content, account }) => {
|
|
254
|
+
}, async ({ fileId, content, account, confirmToken }, ctx) => {
|
|
255
|
+
const got = await runOrDiagnose(['drive', 'get', pos(fileId)], { account });
|
|
256
|
+
if (got.isError) return got;
|
|
257
|
+
const { name } = shareTargetMeta(resultText(got));
|
|
258
|
+
const file = { id: fileId, ...(name !== undefined ? { name } : {}) };
|
|
259
|
+
const mentions = commentMentions(content);
|
|
260
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
261
|
+
action: 'drive.comment-add',
|
|
262
|
+
message: 'Review and confirm this comment — the file\'s owner and anyone mentioned are notified:',
|
|
263
|
+
confirmationLabel: 'Confirm that this comment should be posted now.',
|
|
264
|
+
details: { file, textPreview: bodyPreview(content), mentions },
|
|
265
|
+
unsupportedNote: 'Ask the user to comment from Google Drive.',
|
|
266
|
+
fallback: {
|
|
267
|
+
tool: 'gog_drive_comments_add',
|
|
268
|
+
account,
|
|
269
|
+
confirmToken,
|
|
270
|
+
subject: () => {
|
|
271
|
+
const view = { file, text: content, mentions };
|
|
272
|
+
return { target: fileId, payload: view, preview: view };
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
if (confirmation) return confirmation;
|
|
233
277
|
return runOrDiagnose(['drive', 'comments', 'create', pos(fileId), pos(content)], { account });
|
|
234
278
|
});
|
|
235
279
|
|
|
@@ -259,7 +303,10 @@ export function registerExtraDriveTools(server: McpServer): void {
|
|
|
259
303
|
});
|
|
260
304
|
|
|
261
305
|
server.registerTool('gog_drive_comments_reply', {
|
|
262
|
-
description: 'Reply to an existing comment on a Drive file. Pass `action: "resolve"` or `"reopen"` to atomically flip the parent comment\'s resolved state via the Drive API\'s Reply.action field — avoids the older workaround of deleting the comment (which destroys review-thread context).'
|
|
306
|
+
description: 'Reply to an existing comment on a Drive file. Pass `action: "resolve"` or `"reopen"` to atomically flip the parent comment\'s resolved state via the Drive API\'s Reply.action field — avoids the older workaround of deleting the comment (which destroys review-thread context). '
|
|
307
|
+
+ 'Everyone on the thread and anyone the reply +mentions is notified, so this reads the file and the comment and '
|
|
308
|
+
+ 'asks the MCP host to show the user a confirmation prompt with both and the reply first; nothing is posted unless '
|
|
309
|
+
+ 'they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
263
310
|
annotations: { destructiveHint: false },
|
|
264
311
|
inputSchema: z.object({
|
|
265
312
|
fileId: z.string().describe('File ID'),
|
|
@@ -267,10 +314,36 @@ export function registerExtraDriveTools(server: McpServer): void {
|
|
|
267
314
|
content: z.string().describe('Reply text'),
|
|
268
315
|
action: z.enum(['resolve', 'reopen']).optional().describe('Optional action on the parent comment alongside the reply'),
|
|
269
316
|
account: accountParam,
|
|
317
|
+
confirmToken: confirmTokenParam,
|
|
270
318
|
}),
|
|
271
|
-
}, async ({ fileId, commentId, content, action, account }) => {
|
|
319
|
+
}, async ({ fileId, commentId, content, action, account, confirmToken }, ctx) => {
|
|
272
320
|
const args: GogArg[] = ['drive', 'comments', 'reply', pos(fileId), pos(commentId), pos(content)];
|
|
273
321
|
if (action) args.push(`--action=${action}`);
|
|
322
|
+
const got = await runOrDiagnose(['drive', 'get', pos(fileId)], { account });
|
|
323
|
+
if (got.isError) return got;
|
|
324
|
+
const thread = await runOrDiagnose(['drive', 'comments', 'get', pos(fileId), pos(commentId)], { account });
|
|
325
|
+
if (thread.isError) return thread;
|
|
326
|
+
const { name } = shareTargetMeta(resultText(got));
|
|
327
|
+
const file = { id: fileId, ...(name !== undefined ? { name } : {}) };
|
|
328
|
+
const replyingTo = commentSnapshot(resultText(thread));
|
|
329
|
+
const mentions = commentMentions(content);
|
|
330
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
331
|
+
action: 'drive.comment-reply',
|
|
332
|
+
message: 'Review and confirm this reply — everyone on the thread and anyone mentioned is notified:',
|
|
333
|
+
confirmationLabel: 'Confirm that this reply should be posted now.',
|
|
334
|
+
details: { file, replyingTo, textPreview: bodyPreview(content), mentions, ...(action ? { action } : {}) },
|
|
335
|
+
unsupportedNote: 'Ask the user to reply from Google Drive.',
|
|
336
|
+
fallback: {
|
|
337
|
+
tool: 'gog_drive_comments_reply',
|
|
338
|
+
account,
|
|
339
|
+
confirmToken,
|
|
340
|
+
subject: () => {
|
|
341
|
+
const view = { file, replyingTo, text: content, mentions, ...(action ? { action } : {}) };
|
|
342
|
+
return { target: `${fileId}/${commentId}`, payload: view, preview: view };
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
if (confirmation) return confirmation;
|
|
274
347
|
return runOrDiagnose(args, { account });
|
|
275
348
|
});
|
|
276
349
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { registerExtraDriveTools } from '../../src/tools/drive-extra.js';
|
|
3
|
+
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
+
import { createTestHarness } from '@chrischall/mcp-utils/test';
|
|
5
|
+
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
6
|
+
import type { ElicitRequest, ElicitResult } from '@modelcontextprotocol/server';
|
|
7
|
+
import { pos } from '../../../gogcli-mcp/src/argv.js';
|
|
8
|
+
import { resetConfirmTokenState } from '../../../gogcli-mcp/src/send-confirm-token.js';
|
|
9
|
+
|
|
10
|
+
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof lib>();
|
|
12
|
+
return { ...actual, runOrDiagnose: vi.fn() };
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Fleet audit 2026-09-24 SEC-6: a Drive comment or reply notifies the owner, the
|
|
16
|
+
// thread and everyone it +mentions, so both ask first — naming the file.
|
|
17
|
+
|
|
18
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
process.env = { ...ORIGINAL_ENV };
|
|
22
|
+
delete process.env.MCP_CONFIRM_MODE;
|
|
23
|
+
resetConfirmTokenState();
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => { process.env = ORIGINAL_ENV; });
|
|
26
|
+
|
|
27
|
+
const json = (r: { content: Array<{ text?: string }> }) => JSON.parse(r.content[0]!.text as string);
|
|
28
|
+
async function prompted(answer: ElicitResult = { action: 'accept', content: { confirmed: true } }) {
|
|
29
|
+
const seen: ElicitRequest[] = [];
|
|
30
|
+
const harness = await createTestHarness(registerExtraDriveTools, { elicitation: async (r) => { seen.push(r); return answer; } });
|
|
31
|
+
const details = () => JSON.parse(seen[0]!.params.message.split('\n').slice(1).join('\n')).details;
|
|
32
|
+
return { harness, seen, details };
|
|
33
|
+
}
|
|
34
|
+
const unprompted = () => createTestHarness(registerExtraDriveTools);
|
|
35
|
+
|
|
36
|
+
const FILE = JSON.stringify({ file: { id: 'f1', name: 'Q3 plan', mimeType: 'application/vnd.google-apps.document' } });
|
|
37
|
+
const COMMENT = JSON.stringify({ comment: { id: 'k1', content: 'Is this final?', author: { displayName: 'Alice', emailAddress: 'alice@example.com' }, resolved: false } });
|
|
38
|
+
function stub(failing?: string, file = FILE) {
|
|
39
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args) => {
|
|
40
|
+
const key = args.filter((a): a is string => typeof a === 'string' && !a.startsWith('--')).join(' ');
|
|
41
|
+
if (failing && key.startsWith(failing)) return { content: [{ type: 'text', text: 'Error: not found' }], isError: true };
|
|
42
|
+
if (key === 'drive get') return rawTextResult(file);
|
|
43
|
+
if (key === 'drive comments get') return rawTextResult(COMMENT);
|
|
44
|
+
return rawTextResult('{"ok":true}');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const callsTo = (...words: string[]) => vi.mocked(lib.runOrDiagnose).mock.calls.filter(([args]) =>
|
|
48
|
+
words.every((w, i) => args[i] === w));
|
|
49
|
+
|
|
50
|
+
describe('gog_drive_comments_add', () => {
|
|
51
|
+
const ARGS = { fileId: 'f1', content: 'Looks good +bob@example.com' };
|
|
52
|
+
|
|
53
|
+
it('reads the file and asks with its name, the text and who is mentioned', async () => {
|
|
54
|
+
stub();
|
|
55
|
+
const { harness, details } = await prompted();
|
|
56
|
+
await harness.callTool('gog_drive_comments_add', ARGS);
|
|
57
|
+
expect(details()).toEqual({ file: { id: 'f1', name: 'Q3 plan' }, textPreview: 'Looks good +bob@example.com', mentions: ['bob@example.com'] });
|
|
58
|
+
expect(callsTo('drive', 'comments', 'create')[0]![0]).toEqual(['drive', 'comments', 'create', pos('f1'), pos(ARGS.content)]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('names only the id when the file read carries no name', async () => {
|
|
62
|
+
stub(undefined, '{}');
|
|
63
|
+
const { harness, details } = await prompted({ action: 'decline' });
|
|
64
|
+
await harness.callTool('gog_drive_comments_add', ARGS);
|
|
65
|
+
expect(details().file).toEqual({ id: 'f1' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('posts nothing when the user declines or the read fails', async () => {
|
|
69
|
+
stub();
|
|
70
|
+
expect(json(await (await prompted({ action: 'decline' })).harness.callTool('gog_drive_comments_add', ARGS)))
|
|
71
|
+
.toMatchObject({ cancelled: true, action: 'drive.comment-add' });
|
|
72
|
+
stub('drive get');
|
|
73
|
+
expect((await (await prompted()).harness.callTool('gog_drive_comments_add', ARGS)).isError).toBe(true);
|
|
74
|
+
expect(callsTo('drive', 'comments', 'create')).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('refuses a client that cannot be prompted', async () => {
|
|
78
|
+
process.env.MCP_CONFIRM_MODE = 'refuse';
|
|
79
|
+
stub();
|
|
80
|
+
const r = json(await (await unprompted()).callTool('gog_drive_comments_add', ARGS));
|
|
81
|
+
expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: 'drive.comment-add' });
|
|
82
|
+
expect(r.note).toMatch(/Google Drive/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('token fallback: changed text is DRAFT_CHANGED; the same call posts', async () => {
|
|
86
|
+
process.env.MCP_CONFIRM_MODE = 'ask-user';
|
|
87
|
+
stub();
|
|
88
|
+
const harness = await unprompted();
|
|
89
|
+
const p1 = json(await harness.callTool('gog_drive_comments_add', ARGS));
|
|
90
|
+
expect(p1.preview).toEqual({ file: { id: 'f1', name: 'Q3 plan' }, text: ARGS.content, mentions: ['bob@example.com'] });
|
|
91
|
+
expect(json(await harness.callTool('gog_drive_comments_add', { ...ARGS, content: 'Nope', confirmToken: p1.confirmToken })))
|
|
92
|
+
.toMatchObject({ error: 'DRAFT_CHANGED' });
|
|
93
|
+
await harness.callTool('gog_drive_comments_add', { ...ARGS, confirmToken: p1.confirmToken });
|
|
94
|
+
expect(callsTo('drive', 'comments', 'create')).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('gog_drive_comments_reply', () => {
|
|
99
|
+
const ARGS = { fileId: 'f1', commentId: 'k1', content: 'Yes, final.', action: 'resolve' as const };
|
|
100
|
+
|
|
101
|
+
it('reads the file and the comment and asks with both and the reply', async () => {
|
|
102
|
+
stub();
|
|
103
|
+
const { harness, details } = await prompted();
|
|
104
|
+
await harness.callTool('gog_drive_comments_reply', ARGS);
|
|
105
|
+
expect(details()).toEqual({
|
|
106
|
+
file: { id: 'f1', name: 'Q3 plan' },
|
|
107
|
+
replyingTo: { author: 'Alice <alice@example.com>', content: 'Is this final?', resolved: false },
|
|
108
|
+
textPreview: 'Yes, final.', mentions: [], action: 'resolve',
|
|
109
|
+
});
|
|
110
|
+
expect(callsTo('drive', 'comments', 'reply')[0]![0]).toEqual(
|
|
111
|
+
['drive', 'comments', 'reply', pos('f1'), pos('k1'), pos('Yes, final.'), '--action=resolve']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it.each(['drive get', 'drive comments get'])('returns a failed read (%s) without asking or replying', async (failing) => {
|
|
115
|
+
stub(failing);
|
|
116
|
+
const { harness, seen } = await prompted();
|
|
117
|
+
expect((await harness.callTool('gog_drive_comments_reply', ARGS)).isError).toBe(true);
|
|
118
|
+
expect(seen).toHaveLength(0);
|
|
119
|
+
expect(callsTo('drive', 'comments', 'reply')).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('refuses a client that cannot be prompted', async () => {
|
|
123
|
+
process.env.MCP_CONFIRM_MODE = 'refuse';
|
|
124
|
+
stub();
|
|
125
|
+
const r = json(await (await unprompted()).callTool('gog_drive_comments_reply', ARGS));
|
|
126
|
+
expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: 'drive.comment-reply' });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('token fallback: the thread action is part of what was approved', async () => {
|
|
130
|
+
process.env.MCP_CONFIRM_MODE = 'ask-user';
|
|
131
|
+
stub();
|
|
132
|
+
const harness = await unprompted();
|
|
133
|
+
const p1 = json(await harness.callTool('gog_drive_comments_reply', ARGS));
|
|
134
|
+
expect(p1.preview.action).toBe('resolve');
|
|
135
|
+
expect(json(await harness.callTool('gog_drive_comments_reply', { ...ARGS, action: 'reopen', confirmToken: p1.confirmToken })))
|
|
136
|
+
.toMatchObject({ error: 'DRAFT_CHANGED' });
|
|
137
|
+
expect(callsTo('drive', 'comments', 'reply')).toHaveLength(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('token fallback: previews the thread; a reply with no action posts on phase 2', async () => {
|
|
141
|
+
process.env.MCP_CONFIRM_MODE = 'ask-user';
|
|
142
|
+
stub();
|
|
143
|
+
const harness = await unprompted();
|
|
144
|
+
const args = { fileId: 'f1', commentId: 'k1', content: 'Yes.' };
|
|
145
|
+
const p1 = json(await harness.callTool('gog_drive_comments_reply', args));
|
|
146
|
+
expect(p1.preview).toEqual({
|
|
147
|
+
file: { id: 'f1', name: 'Q3 plan' },
|
|
148
|
+
replyingTo: { author: 'Alice <alice@example.com>', content: 'Is this final?', resolved: false },
|
|
149
|
+
text: 'Yes.', mentions: [],
|
|
150
|
+
});
|
|
151
|
+
await harness.callTool('gog_drive_comments_reply', { ...args, confirmToken: p1.confirmToken });
|
|
152
|
+
expect(callsTo('drive', 'comments', 'reply')).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -18,7 +18,10 @@ let harness: TestHarness;
|
|
|
18
18
|
beforeEach(async () => {
|
|
19
19
|
vi.clearAllMocks();
|
|
20
20
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
21
|
-
|
|
21
|
+
// The comment tools ask first (drive-extra-gates.test.ts covers the prompts); here the user accepts.
|
|
22
|
+
harness = await createTestHarness(registerExtraDriveTools, {
|
|
23
|
+
elicitation: async () => ({ action: 'accept', content: { confirmed: true } }),
|
|
24
|
+
});
|
|
22
25
|
});
|
|
23
26
|
|
|
24
27
|
describe('gog_drive_download', () => {
|