gogcli-mcp-calendar 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/dist/index.js +297 -74
- package/manifest.json +5 -5
- package/package.json +2 -2
- package/src/tools/calendar-extra.ts +114 -9
- package/tests/tools/calendar-extra-gates.test.ts +221 -0
- package/tests/tools/calendar-extra.test.ts +5 -1
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. */
|
|
@@ -35946,6 +36023,7 @@ async function diagnose(err) {
|
|
|
35946
36023
|
const isAuthError = !isTransientError && AUTH_ERROR_PATTERN.test(errText);
|
|
35947
36024
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
35948
36025
|
const hint = isInvalidGrant ? INVALID_GRANT_HINT : isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
|
|
36026
|
+
if (!isAuthError && !isInvalidGrant) return errorResult(`${errText}${hint}`);
|
|
35949
36027
|
try {
|
|
35950
36028
|
const accounts = formatAccountList(await run(["auth", "list"]));
|
|
35951
36029
|
return errorResult(`${errText}
|
|
@@ -36043,8 +36121,20 @@ function confirmSpentStore() {
|
|
|
36043
36121
|
// ../gogcli-mcp/src/dispatch-confirmation.ts
|
|
36044
36122
|
var CONFIRM_ACTION_INSTRUCTION = CONFIRM_TOKEN_INSTRUCTION;
|
|
36045
36123
|
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.';
|
|
36124
|
+
function refusedInRun(what, via, does, instead) {
|
|
36125
|
+
return `${what} ${does} and is not available through ${via}. ${instead}`;
|
|
36126
|
+
}
|
|
36046
36127
|
function gatedElsewhere(what, via, does, tool) {
|
|
36047
|
-
return
|
|
36128
|
+
return refusedInRun(what, via, does, `Use ${tool}, which asks the user to confirm.`);
|
|
36129
|
+
}
|
|
36130
|
+
function flagValue(args, name) {
|
|
36131
|
+
const flag = `--${name}`;
|
|
36132
|
+
for (let i = 0; i < args.length; i++) {
|
|
36133
|
+
const a = args[i];
|
|
36134
|
+
if (a === flag) return args[i + 1] ?? "";
|
|
36135
|
+
if (a.startsWith(`${flag}=`)) return a.slice(flag.length + 1);
|
|
36136
|
+
}
|
|
36137
|
+
return void 0;
|
|
36048
36138
|
}
|
|
36049
36139
|
async function requireDispatchConfirmation(ctx, options) {
|
|
36050
36140
|
const { action, fallback } = options;
|
|
@@ -36206,21 +36296,47 @@ function authToolsFor(defaultServices) {
|
|
|
36206
36296
|
}
|
|
36207
36297
|
|
|
36208
36298
|
// ../gogcli-mcp/src/tools/calendar.ts
|
|
36209
|
-
var
|
|
36210
|
-
|
|
36211
|
-
|
|
36212
|
-
|
|
36213
|
-
|
|
36214
|
-
|
|
36215
|
-
|
|
36216
|
-
|
|
36217
|
-
|
|
36218
|
-
|
|
36299
|
+
var ASKS = (tool) => `Use ${tool}, which asks the user to confirm.`;
|
|
36300
|
+
var CALENDAR_REFUSED = {
|
|
36301
|
+
create: { does: "can change what guests see", instead: ASKS("gog_calendar_create") },
|
|
36302
|
+
add: { does: "can change what guests see", instead: ASKS("gog_calendar_create") },
|
|
36303
|
+
new: { does: "can change what guests see", instead: ASKS("gog_calendar_create") },
|
|
36304
|
+
update: { does: "can change what guests see", instead: ASKS("gog_calendar_update") },
|
|
36305
|
+
edit: { does: "can change what guests see", instead: ASKS("gog_calendar_update") },
|
|
36306
|
+
set: { does: "can change what guests see", instead: ASKS("gog_calendar_update") },
|
|
36307
|
+
respond: { does: "can change what guests see", instead: ASKS("gog_calendar_respond") },
|
|
36308
|
+
rsvp: { does: "can change what guests see", instead: ASKS("gog_calendar_respond") },
|
|
36309
|
+
reply: { does: "can change what guests see", instead: ASKS("gog_calendar_respond") },
|
|
36310
|
+
move: { does: "moves an event off guests' calendars (and can email them)", instead: ASKS("gog_calendar_move") },
|
|
36311
|
+
transfer: { does: "moves an event off guests' calendars (and can email them)", instead: ASKS("gog_calendar_move") },
|
|
36312
|
+
delete: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36313
|
+
del: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36314
|
+
rm: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36315
|
+
remove: { does: "removes an event from every guest's calendar (the whole series by default)", instead: ASKS("gog_calendar_delete") },
|
|
36316
|
+
"delete-calendar": { does: "deletes a calendar and every event on it", instead: ASKS("gog_calendar_delete_calendar") },
|
|
36317
|
+
"out-of-office": { does: "auto-declines invitations and notifies their organizers", instead: ASKS("gog_calendar_out_of_office") },
|
|
36318
|
+
ooo: { does: "auto-declines invitations and notifies their organizers", instead: ASKS("gog_calendar_out_of_office") }
|
|
36219
36319
|
};
|
|
36220
|
-
function vetCalendarRun(subcommand,
|
|
36320
|
+
function vetCalendarRun(subcommand, args) {
|
|
36221
36321
|
const sub = subcommand.toLowerCase();
|
|
36222
|
-
const
|
|
36223
|
-
|
|
36322
|
+
const what = `gog calendar ${sub}`;
|
|
36323
|
+
const via = "gog_calendar_run";
|
|
36324
|
+
if (Object.hasOwn(CALENDAR_REFUSED, sub)) {
|
|
36325
|
+
const { does, instead } = CALENDAR_REFUSED[sub];
|
|
36326
|
+
return refusedInRun(what, via, does, instead);
|
|
36327
|
+
}
|
|
36328
|
+
if (sub === "propose-time" && (flagValue(args, "decline") !== void 0 || flagValue(args, "comment") !== void 0)) {
|
|
36329
|
+
return gatedElsewhere(`${what} --decline`, via, "declines the event and notifies the organizer", "gog_calendar_respond");
|
|
36330
|
+
}
|
|
36331
|
+
if ((sub === "focus-time" || sub === "focus") && flagValue(args, "auto-decline")?.toLowerCase() !== "none") {
|
|
36332
|
+
return refusedInRun(
|
|
36333
|
+
what,
|
|
36334
|
+
via,
|
|
36335
|
+
"auto-declines invitations and notifies their organizers (gog defaults --auto-decline to all)",
|
|
36336
|
+
"Pass --auto-decline=none, or ask the user to create it from Google Calendar."
|
|
36337
|
+
);
|
|
36338
|
+
}
|
|
36339
|
+
return void 0;
|
|
36224
36340
|
}
|
|
36225
36341
|
function parseAttendees(value) {
|
|
36226
36342
|
if (!value) return [];
|
|
@@ -36245,6 +36361,8 @@ function eventSnapshot(raw) {
|
|
|
36245
36361
|
organizer: str(event?.organizer?.email),
|
|
36246
36362
|
// Guests are the OTHER people: not this account's own attendee row, not rooms.
|
|
36247
36363
|
guests: (Array.isArray(event?.attendees) ? event.attendees : []).filter((a) => a.self !== true && a.resource !== true && typeof a.email === "string").map((a) => a.email),
|
|
36364
|
+
// A series, or one instance of one: gog's delete --scope defaults to all.
|
|
36365
|
+
...Array.isArray(event?.recurrence) && event.recurrence.length > 0 || typeof event?.recurringEventId === "string" ? { recurring: true } : {},
|
|
36248
36366
|
etag: str(event?.etag)
|
|
36249
36367
|
};
|
|
36250
36368
|
}
|
|
@@ -36466,14 +36584,35 @@ function registerCalendarTools(server) {
|
|
|
36466
36584
|
return runOrDiagnose(args, { account });
|
|
36467
36585
|
});
|
|
36468
36586
|
server.registerTool("gog_calendar_delete", {
|
|
36469
|
-
description: "Delete a calendar event.",
|
|
36587
|
+
description: "Delete a calendar event. It disappears from every guest's calendar too, and for a recurring event gog deletes the WHOLE series, so when the event has guests or recurs this reads it and asks the MCP host to show the user a confirmation prompt with the event as it stands; a guest-free, one-off event is deleted without asking." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36470
36588
|
annotations: { destructiveHint: true },
|
|
36471
36589
|
inputSchema: external_exports.object({
|
|
36472
36590
|
calendarId: external_exports.string().describe("Calendar ID"),
|
|
36473
36591
|
eventId: external_exports.string().describe("Event ID"),
|
|
36474
|
-
account: accountParam
|
|
36592
|
+
account: accountParam,
|
|
36593
|
+
confirmToken: confirmTokenParam
|
|
36475
36594
|
})
|
|
36476
|
-
}, async ({ calendarId, eventId, account }) => {
|
|
36595
|
+
}, async ({ calendarId, eventId, account, confirmToken }, ctx) => {
|
|
36596
|
+
const read = await readEvent(calendarId, eventId, account);
|
|
36597
|
+
if (read.error) return read.error;
|
|
36598
|
+
const { etag, ...event } = read.event;
|
|
36599
|
+
if (event.guests.length > 0 || event.recurring) {
|
|
36600
|
+
const view = { calendarId, eventId, event, scope: event.recurring ? "the whole recurring series" : "this event" };
|
|
36601
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
36602
|
+
action: "calendar.delete",
|
|
36603
|
+
message: event.recurring ? "Review and confirm deleting this ENTIRE recurring series:" : "Review and confirm deleting this event from every guest's calendar:",
|
|
36604
|
+
confirmationLabel: "Confirm that this event should be deleted now.",
|
|
36605
|
+
details: view,
|
|
36606
|
+
unsupportedNote: "Ask the user to delete it from Google Calendar.",
|
|
36607
|
+
fallback: {
|
|
36608
|
+
tool: "gog_calendar_delete",
|
|
36609
|
+
account,
|
|
36610
|
+
confirmToken,
|
|
36611
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
36612
|
+
}
|
|
36613
|
+
});
|
|
36614
|
+
if (confirmation) return confirmation;
|
|
36615
|
+
}
|
|
36477
36616
|
return runOrDiagnose(["calendar", "delete", pos(calendarId), pos(eventId), "--force"], { account });
|
|
36478
36617
|
});
|
|
36479
36618
|
server.registerTool("gog_calendar_respond", {
|
|
@@ -36566,7 +36705,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
36566
36705
|
);
|
|
36567
36706
|
|
|
36568
36707
|
// ../gogcli-mcp/src/server.ts
|
|
36569
|
-
var VERSION = true ? "4.5.
|
|
36708
|
+
var VERSION = true ? "4.5.2" : "0.0.0";
|
|
36570
36709
|
|
|
36571
36710
|
// ../gogcli-mcp/src/bootstrap-auth.ts
|
|
36572
36711
|
import { join as join5 } from "node:path";
|
|
@@ -36642,6 +36781,20 @@ var MAX_BLOB_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
|
36642
36781
|
|
|
36643
36782
|
// src/tools/calendar-extra.ts
|
|
36644
36783
|
var meetAccess = external_exports.enum(["open", "trusted", "restricted"]);
|
|
36784
|
+
function calendarSnapshot(raw, calendarId) {
|
|
36785
|
+
let cal;
|
|
36786
|
+
try {
|
|
36787
|
+
const parsed = JSON.parse(raw);
|
|
36788
|
+
cal = parsed?.result ?? parsed ?? void 0;
|
|
36789
|
+
} catch {
|
|
36790
|
+
cal = void 0;
|
|
36791
|
+
}
|
|
36792
|
+
return {
|
|
36793
|
+
id: calendarId,
|
|
36794
|
+
...typeof cal?.summary === "string" ? { summary: cal.summary } : {},
|
|
36795
|
+
...typeof cal?.etag === "string" ? { etag: cal.etag } : {}
|
|
36796
|
+
};
|
|
36797
|
+
}
|
|
36645
36798
|
function registerExtraCalendarTools(server) {
|
|
36646
36799
|
server.registerTool("gog_meet_create", {
|
|
36647
36800
|
description: "Create a Google Meet space and return its meeting code.",
|
|
@@ -36856,34 +37009,56 @@ function registerExtraCalendarTools(server) {
|
|
|
36856
37009
|
return runOrDiagnose(args, { account });
|
|
36857
37010
|
});
|
|
36858
37011
|
server.registerTool("gog_calendar_move", {
|
|
36859
|
-
description: "Move an event from one calendar to another; the destination calendar becomes the organizer.",
|
|
37012
|
+
description: "Move an event from one calendar to another; the destination calendar becomes the organizer. With sendUpdates all or externalOnly Google emails the guests, so this reads the event and asks the MCP host to show the user a confirmation prompt with the event, its guests and the destination first; a move that notifies nobody (the default) is made without asking." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36860
37013
|
annotations: { destructiveHint: false },
|
|
36861
37014
|
inputSchema: external_exports.object({
|
|
36862
37015
|
calendarId: external_exports.string().describe("Source calendar ID"),
|
|
36863
37016
|
eventId: external_exports.string().describe("Event ID"),
|
|
36864
37017
|
destinationCalendarId: external_exports.string().describe("Destination calendar ID that becomes the event organizer"),
|
|
36865
|
-
sendUpdates: external_exports.enum(["all", "externalOnly", "none"]).optional().describe("Notification mode (default: none)"),
|
|
36866
|
-
account: accountParam
|
|
37018
|
+
sendUpdates: external_exports.enum(["all", "externalOnly", "none"]).optional().describe("Notification mode (default: none). all / externalOnly email the guests and ask the user first."),
|
|
37019
|
+
account: accountParam,
|
|
37020
|
+
confirmToken: confirmTokenParam
|
|
36867
37021
|
})
|
|
36868
|
-
}, async ({ calendarId, eventId, destinationCalendarId, sendUpdates, account }) => {
|
|
37022
|
+
}, async ({ calendarId, eventId, destinationCalendarId, sendUpdates, account, confirmToken }, ctx) => {
|
|
36869
37023
|
const args = ["calendar", "move", pos(calendarId), pos(eventId), pos(destinationCalendarId)];
|
|
36870
37024
|
if (sendUpdates) args.push(`--send-updates=${sendUpdates}`);
|
|
37025
|
+
if (sendUpdates === "all" || sendUpdates === "externalOnly") {
|
|
37026
|
+
const got = await runOrDiagnose(["calendar", "event", pos(calendarId), pos(eventId)], { account });
|
|
37027
|
+
if (got.isError) return got;
|
|
37028
|
+
const { etag, ...event } = eventSnapshot(resultText(got));
|
|
37029
|
+
const view = { calendarId, eventId, event, destinationCalendarId, emails: sendUpdates === "all" ? "every guest" : "guests outside your domain" };
|
|
37030
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37031
|
+
action: "calendar.move",
|
|
37032
|
+
message: "Review and confirm moving this event \u2014 Google will email the guests:",
|
|
37033
|
+
confirmationLabel: "Confirm that this event should be moved and the guests notified now.",
|
|
37034
|
+
details: view,
|
|
37035
|
+
unsupportedNote: "Move it with sendUpdates none instead, which notifies nobody.",
|
|
37036
|
+
fallback: {
|
|
37037
|
+
tool: "gog_calendar_move",
|
|
37038
|
+
account,
|
|
37039
|
+
confirmToken,
|
|
37040
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view })
|
|
37041
|
+
}
|
|
37042
|
+
});
|
|
37043
|
+
if (confirmation) return confirmation;
|
|
37044
|
+
}
|
|
36871
37045
|
return runOrDiagnose(args, { account });
|
|
36872
37046
|
});
|
|
36873
37047
|
server.registerTool("gog_calendar_out_of_office", {
|
|
36874
|
-
description: "Create an Out of Office event that auto-declines invitations during the block.",
|
|
37048
|
+
description: "Create an Out of Office event that auto-declines invitations during the block. gog auto-declines EVERY conflicting meeting by default (autoDecline all), and each organizer is notified, so unless autoDecline is none this asks the MCP host to show the user a confirmation prompt with the window, the decline mode and the message first; nothing is created unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36875
37049
|
annotations: { destructiveHint: true },
|
|
36876
37050
|
inputSchema: external_exports.object({
|
|
36877
37051
|
from: external_exports.string().describe("Start date or datetime (RFC3339 or YYYY-MM-DD)"),
|
|
36878
37052
|
to: external_exports.string().describe("End date or datetime (RFC3339 or YYYY-MM-DD)"),
|
|
36879
37053
|
calendarId: external_exports.string().optional().describe("Calendar ID (default: primary)"),
|
|
36880
37054
|
summary: external_exports.string().optional().describe('Out of office title (default: "Out of office")'),
|
|
36881
|
-
autoDecline: external_exports.enum(["none", "all", "new"]).optional().describe("Auto-decline mode (default: all)"),
|
|
37055
|
+
autoDecline: external_exports.enum(["none", "all", "new"]).optional().describe("Auto-decline mode (default: all \u2014 declines existing AND new conflicting meetings; none notifies nobody and does not ask)"),
|
|
36882
37056
|
declineMessage: external_exports.string().optional().describe("Message for declined invitations"),
|
|
36883
37057
|
allDay: external_exports.boolean().optional().describe("Create as an all-day event"),
|
|
36884
|
-
account: accountParam
|
|
37058
|
+
account: accountParam,
|
|
37059
|
+
confirmToken: confirmTokenParam
|
|
36885
37060
|
})
|
|
36886
|
-
}, async ({ from, to, calendarId, summary, autoDecline, declineMessage, allDay, account }) => {
|
|
37061
|
+
}, async ({ from, to, calendarId, summary, autoDecline, declineMessage, allDay, account, confirmToken }, ctx) => {
|
|
36887
37062
|
const args = ["calendar", "out-of-office"];
|
|
36888
37063
|
if (calendarId) args.push(pos(calendarId));
|
|
36889
37064
|
args.push(`--from=${from}`);
|
|
@@ -36892,6 +37067,32 @@ function registerExtraCalendarTools(server) {
|
|
|
36892
37067
|
if (autoDecline) args.push(`--auto-decline=${autoDecline}`);
|
|
36893
37068
|
if (declineMessage) args.push(`--decline-message=${declineMessage}`);
|
|
36894
37069
|
if (allDay) args.push("--all-day");
|
|
37070
|
+
if (autoDecline !== "none") {
|
|
37071
|
+
const mode = autoDecline ?? "all";
|
|
37072
|
+
const view = {
|
|
37073
|
+
calendarId: calendarId ?? "primary",
|
|
37074
|
+
from,
|
|
37075
|
+
to,
|
|
37076
|
+
allDay: Boolean(allDay),
|
|
37077
|
+
summary: summary ?? "Out of office",
|
|
37078
|
+
declines: mode === "all" ? "every existing and new conflicting meeting" : "new conflicting invitations",
|
|
37079
|
+
declineMessage
|
|
37080
|
+
};
|
|
37081
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37082
|
+
action: "calendar.out-of-office",
|
|
37083
|
+
message: "Review and confirm this Out of Office block \u2014 conflicting meetings are declined and their organizers notified:",
|
|
37084
|
+
confirmationLabel: "Confirm that meetings in this window should be auto-declined.",
|
|
37085
|
+
details: view,
|
|
37086
|
+
unsupportedNote: "Create it with autoDecline none instead; the user can turn on auto-decline in Google Calendar.",
|
|
37087
|
+
fallback: {
|
|
37088
|
+
tool: "gog_calendar_out_of_office",
|
|
37089
|
+
account,
|
|
37090
|
+
confirmToken,
|
|
37091
|
+
subject: () => ({ target: view.calendarId, payload: view, preview: view })
|
|
37092
|
+
}
|
|
37093
|
+
});
|
|
37094
|
+
if (confirmation) return confirmation;
|
|
37095
|
+
}
|
|
36895
37096
|
return runOrDiagnose(args, { account });
|
|
36896
37097
|
});
|
|
36897
37098
|
server.registerTool("gog_calendar_unsubscribe", {
|
|
@@ -36905,13 +37106,35 @@ function registerExtraCalendarTools(server) {
|
|
|
36905
37106
|
return runOrDiagnose(["calendar", "unsubscribe", pos(calendarId)], { account });
|
|
36906
37107
|
});
|
|
36907
37108
|
server.registerTool("gog_calendar_delete_calendar", {
|
|
36908
|
-
description: "Permanently delete an owned secondary calendar and all its events. Cannot delete your primary calendar. To merely remove a calendar you do not own from your list, use gog_calendar_unsubscribe.",
|
|
37109
|
+
description: "Permanently delete an owned secondary calendar and all its events. Cannot delete your primary calendar. To merely remove a calendar you do not own from your list, use gog_calendar_unsubscribe. Reads the calendar and asks the MCP host to show the user a confirmation prompt naming it first; nothing is deleted unless they accept." + CONFIRM_FALLBACK_DESCRIPTION,
|
|
36909
37110
|
annotations: { destructiveHint: true },
|
|
36910
37111
|
inputSchema: external_exports.object({
|
|
36911
37112
|
calendarId: external_exports.string().describe("Owned secondary calendar ID or alias"),
|
|
36912
|
-
account: accountParam
|
|
37113
|
+
account: accountParam,
|
|
37114
|
+
confirmToken: confirmTokenParam
|
|
36913
37115
|
})
|
|
36914
|
-
}, async ({ calendarId, account }) => {
|
|
37116
|
+
}, async ({ calendarId, account, confirmToken }, ctx) => {
|
|
37117
|
+
const got = await runOrDiagnose(
|
|
37118
|
+
["api", "call", "calendar", "v3", "calendars.get", `--params=${JSON.stringify({ calendarId })}`],
|
|
37119
|
+
{ account }
|
|
37120
|
+
);
|
|
37121
|
+
if (got.isError) return got;
|
|
37122
|
+
const { etag, ...calendar } = calendarSnapshot(resultText(got), calendarId);
|
|
37123
|
+
const view = { calendar, deletes: "the calendar and every event on it, for good" };
|
|
37124
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
37125
|
+
action: "calendar.delete-calendar",
|
|
37126
|
+
message: "Review and confirm PERMANENTLY deleting this calendar and all of its events:",
|
|
37127
|
+
confirmationLabel: "Confirm that this calendar should be deleted now.",
|
|
37128
|
+
details: view,
|
|
37129
|
+
unsupportedNote: "Ask the user to delete it from Google Calendar's settings, or use gog_calendar_unsubscribe to only hide it.",
|
|
37130
|
+
fallback: {
|
|
37131
|
+
tool: "gog_calendar_delete_calendar",
|
|
37132
|
+
account,
|
|
37133
|
+
confirmToken,
|
|
37134
|
+
subject: () => ({ target: calendarId, revision: etag, payload: view, preview: view })
|
|
37135
|
+
}
|
|
37136
|
+
});
|
|
37137
|
+
if (confirmation) return confirmation;
|
|
36915
37138
|
return runOrDiagnose(["calendar", "delete-calendar", pos(calendarId), "--force"], { account });
|
|
36916
37139
|
});
|
|
36917
37140
|
server.registerTool("gog_meet_participants", {
|
|
@@ -36946,7 +37169,7 @@ await runMcp({
|
|
|
36946
37169
|
});
|
|
36947
37170
|
/*! Bundled license information:
|
|
36948
37171
|
|
|
36949
|
-
@modelcontextprotocol/server/dist/src-
|
|
37172
|
+
@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs:
|
|
36950
37173
|
(*!
|
|
36951
37174
|
* content-type
|
|
36952
37175
|
* 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-calendar",
|
|
5
5
|
"display_name": "gogcli (Calendar)",
|
|
6
|
-
"version": "4.5.
|
|
6
|
+
"version": "4.5.2",
|
|
7
7
|
"description": "Extended Google Calendar for Claude via gogcli — auth + Calendar events + Meet space management",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"name": "gog_calendar_delete",
|
|
114
|
-
"description": "Delete a calendar event"
|
|
114
|
+
"description": "Delete a calendar event (asks the user to confirm when it has guests or recurs)"
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
"name": "gog_calendar_respond",
|
|
@@ -147,11 +147,11 @@
|
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
"name": "gog_calendar_move",
|
|
150
|
-
"description": "Move an event to another calendar"
|
|
150
|
+
"description": "Move an event to another calendar (asks the user to confirm when guests are emailed)"
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"name": "gog_calendar_out_of_office",
|
|
154
|
-
"description": "Create an Out of Office event that auto-declines invitations"
|
|
154
|
+
"description": "Create an Out of Office event that auto-declines invitations (asks the user to confirm unless autoDecline is none)"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"name": "gog_calendar_unsubscribe",
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
},
|
|
160
160
|
{
|
|
161
161
|
"name": "gog_calendar_delete_calendar",
|
|
162
|
-
"description": "Delete an owned secondary calendar"
|
|
162
|
+
"description": "Delete an owned secondary calendar (asks the user to confirm)"
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
"name": "gog_meet_create",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-calendar",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-calendar",
|
|
5
5
|
"description": "Extended Google Calendar + Meet MCP server via gogcli — auth + Calendar events + Meet space management",
|
|
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",
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
accountParam,
|
|
5
|
+
runOrDiagnose,
|
|
6
|
+
pageTokenParam,
|
|
7
|
+
pageAliasParam,
|
|
8
|
+
resolvePageToken,
|
|
9
|
+
pos,
|
|
10
|
+
CONFIRM_FALLBACK_DESCRIPTION,
|
|
11
|
+
confirmTokenParam,
|
|
12
|
+
eventSnapshot,
|
|
13
|
+
requireDispatchConfirmation,
|
|
14
|
+
resultText,
|
|
15
|
+
} from '../../../gogcli-mcp/src/lib.js';
|
|
4
16
|
import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
|
|
5
17
|
|
|
6
18
|
const meetAccess = z.enum(['open', 'trusted', 'restricted']);
|
|
7
19
|
|
|
20
|
+
/** A calendar by name, for a delete's prompt; `etag` apart as the token's revision. Unreadable output names nothing. */
|
|
21
|
+
export function calendarSnapshot(raw: string, calendarId: string): { id: string; summary?: string; etag?: string } {
|
|
22
|
+
let cal: { summary?: unknown; etag?: unknown } | undefined;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw) as ({ result?: typeof cal } & NonNullable<typeof cal>) | null;
|
|
25
|
+
cal = parsed?.result ?? parsed ?? undefined;
|
|
26
|
+
} catch {
|
|
27
|
+
cal = undefined;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
id: calendarId,
|
|
31
|
+
...(typeof cal?.summary === 'string' ? { summary: cal.summary } : {}),
|
|
32
|
+
...(typeof cal?.etag === 'string' ? { etag: cal.etag } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
// Meet spaces are the conferencing surface attached to calendar events,
|
|
9
37
|
// so they live in the calendar sub-package.
|
|
10
38
|
export function registerExtraCalendarTools(server: McpServer): void {
|
|
@@ -241,35 +269,63 @@ export function registerExtraCalendarTools(server: McpServer): void {
|
|
|
241
269
|
});
|
|
242
270
|
|
|
243
271
|
server.registerTool('gog_calendar_move', {
|
|
244
|
-
description: 'Move an event from one calendar to another; the destination calendar becomes the organizer.'
|
|
272
|
+
description: 'Move an event from one calendar to another; the destination calendar becomes the organizer. With '
|
|
273
|
+
+ 'sendUpdates all or externalOnly Google emails the guests, so this reads the event and asks the MCP host to '
|
|
274
|
+
+ 'show the user a confirmation prompt with the event, its guests and the destination first; a move that '
|
|
275
|
+
+ 'notifies nobody (the default) is made without asking.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
245
276
|
annotations: { destructiveHint: false },
|
|
246
277
|
inputSchema: z.object({
|
|
247
278
|
calendarId: z.string().describe('Source calendar ID'),
|
|
248
279
|
eventId: z.string().describe('Event ID'),
|
|
249
280
|
destinationCalendarId: z.string().describe('Destination calendar ID that becomes the event organizer'),
|
|
250
|
-
sendUpdates: z.enum(['all', 'externalOnly', 'none']).optional().describe('Notification mode (default: none)'),
|
|
281
|
+
sendUpdates: z.enum(['all', 'externalOnly', 'none']).optional().describe('Notification mode (default: none). all / externalOnly email the guests and ask the user first.'),
|
|
251
282
|
account: accountParam,
|
|
283
|
+
confirmToken: confirmTokenParam,
|
|
252
284
|
}),
|
|
253
|
-
}, async ({ calendarId, eventId, destinationCalendarId, sendUpdates, account }) => {
|
|
285
|
+
}, async ({ calendarId, eventId, destinationCalendarId, sendUpdates, account, confirmToken }, ctx) => {
|
|
254
286
|
const args: GogArg[] = ['calendar', 'move', pos(calendarId), pos(eventId), pos(destinationCalendarId)];
|
|
255
287
|
if (sendUpdates) args.push(`--send-updates=${sendUpdates}`);
|
|
288
|
+
if (sendUpdates === 'all' || sendUpdates === 'externalOnly') {
|
|
289
|
+
const got = await runOrDiagnose(['calendar', 'event', pos(calendarId), pos(eventId)], { account });
|
|
290
|
+
if (got.isError) return got;
|
|
291
|
+
const { etag, ...event } = eventSnapshot(resultText(got));
|
|
292
|
+
const view = { calendarId, eventId, event, destinationCalendarId, emails: sendUpdates === 'all' ? 'every guest' : 'guests outside your domain' };
|
|
293
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
294
|
+
action: 'calendar.move',
|
|
295
|
+
message: 'Review and confirm moving this event — Google will email the guests:',
|
|
296
|
+
confirmationLabel: 'Confirm that this event should be moved and the guests notified now.',
|
|
297
|
+
details: view,
|
|
298
|
+
unsupportedNote: 'Move it with sendUpdates none instead, which notifies nobody.',
|
|
299
|
+
fallback: {
|
|
300
|
+
tool: 'gog_calendar_move',
|
|
301
|
+
account,
|
|
302
|
+
confirmToken,
|
|
303
|
+
subject: () => ({ target: `${calendarId}/${eventId}`, revision: etag, payload: view, preview: view }),
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
if (confirmation) return confirmation;
|
|
307
|
+
}
|
|
256
308
|
return runOrDiagnose(args, { account });
|
|
257
309
|
});
|
|
258
310
|
|
|
259
311
|
server.registerTool('gog_calendar_out_of_office', {
|
|
260
|
-
description: 'Create an Out of Office event that auto-declines invitations during the block.'
|
|
312
|
+
description: 'Create an Out of Office event that auto-declines invitations during the block. gog auto-declines '
|
|
313
|
+
+ 'EVERY conflicting meeting by default (autoDecline all), and each organizer is notified, so unless autoDecline '
|
|
314
|
+
+ 'is none this asks the MCP host to show the user a confirmation prompt with the window, the decline mode and '
|
|
315
|
+
+ 'the message first; nothing is created unless they accept.' + CONFIRM_FALLBACK_DESCRIPTION,
|
|
261
316
|
annotations: { destructiveHint: true },
|
|
262
317
|
inputSchema: z.object({
|
|
263
318
|
from: z.string().describe('Start date or datetime (RFC3339 or YYYY-MM-DD)'),
|
|
264
319
|
to: z.string().describe('End date or datetime (RFC3339 or YYYY-MM-DD)'),
|
|
265
320
|
calendarId: z.string().optional().describe('Calendar ID (default: primary)'),
|
|
266
321
|
summary: z.string().optional().describe('Out of office title (default: "Out of office")'),
|
|
267
|
-
autoDecline: z.enum(['none', 'all', 'new']).optional().describe('Auto-decline mode (default: all)'),
|
|
322
|
+
autoDecline: z.enum(['none', 'all', 'new']).optional().describe('Auto-decline mode (default: all — declines existing AND new conflicting meetings; none notifies nobody and does not ask)'),
|
|
268
323
|
declineMessage: z.string().optional().describe('Message for declined invitations'),
|
|
269
324
|
allDay: z.boolean().optional().describe('Create as an all-day event'),
|
|
270
325
|
account: accountParam,
|
|
326
|
+
confirmToken: confirmTokenParam,
|
|
271
327
|
}),
|
|
272
|
-
}, async ({ from, to, calendarId, summary, autoDecline, declineMessage, allDay, account }) => {
|
|
328
|
+
}, async ({ from, to, calendarId, summary, autoDecline, declineMessage, allDay, account, confirmToken }, ctx) => {
|
|
273
329
|
const args: GogArg[] = ['calendar', 'out-of-office'];
|
|
274
330
|
if (calendarId) args.push(pos(calendarId));
|
|
275
331
|
args.push(`--from=${from}`);
|
|
@@ -278,6 +334,32 @@ export function registerExtraCalendarTools(server: McpServer): void {
|
|
|
278
334
|
if (autoDecline) args.push(`--auto-decline=${autoDecline}`);
|
|
279
335
|
if (declineMessage) args.push(`--decline-message=${declineMessage}`);
|
|
280
336
|
if (allDay) args.push('--all-day');
|
|
337
|
+
if (autoDecline !== 'none') {
|
|
338
|
+
const mode = autoDecline ?? 'all';
|
|
339
|
+
const view = {
|
|
340
|
+
calendarId: calendarId ?? 'primary',
|
|
341
|
+
from,
|
|
342
|
+
to,
|
|
343
|
+
allDay: Boolean(allDay),
|
|
344
|
+
summary: summary ?? 'Out of office',
|
|
345
|
+
declines: mode === 'all' ? 'every existing and new conflicting meeting' : 'new conflicting invitations',
|
|
346
|
+
declineMessage,
|
|
347
|
+
};
|
|
348
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
349
|
+
action: 'calendar.out-of-office',
|
|
350
|
+
message: 'Review and confirm this Out of Office block — conflicting meetings are declined and their organizers notified:',
|
|
351
|
+
confirmationLabel: 'Confirm that meetings in this window should be auto-declined.',
|
|
352
|
+
details: view,
|
|
353
|
+
unsupportedNote: 'Create it with autoDecline none instead; the user can turn on auto-decline in Google Calendar.',
|
|
354
|
+
fallback: {
|
|
355
|
+
tool: 'gog_calendar_out_of_office',
|
|
356
|
+
account,
|
|
357
|
+
confirmToken,
|
|
358
|
+
subject: () => ({ target: view.calendarId, payload: view, preview: view }),
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
if (confirmation) return confirmation;
|
|
362
|
+
}
|
|
281
363
|
return runOrDiagnose(args, { account });
|
|
282
364
|
});
|
|
283
365
|
|
|
@@ -293,13 +375,36 @@ export function registerExtraCalendarTools(server: McpServer): void {
|
|
|
293
375
|
});
|
|
294
376
|
|
|
295
377
|
server.registerTool('gog_calendar_delete_calendar', {
|
|
296
|
-
description: 'Permanently delete an owned secondary calendar and all its events. Cannot delete your primary calendar.
|
|
378
|
+
description: 'Permanently delete an owned secondary calendar and all its events. Cannot delete your primary calendar. '
|
|
379
|
+
+ 'To merely remove a calendar you do not own from your list, use gog_calendar_unsubscribe. Reads the calendar and '
|
|
380
|
+
+ 'asks the MCP host to show the user a confirmation prompt naming it first; nothing is deleted unless they accept.'
|
|
381
|
+
+ CONFIRM_FALLBACK_DESCRIPTION,
|
|
297
382
|
annotations: { destructiveHint: true },
|
|
298
383
|
inputSchema: z.object({
|
|
299
384
|
calendarId: z.string().describe('Owned secondary calendar ID or alias'),
|
|
300
385
|
account: accountParam,
|
|
386
|
+
confirmToken: confirmTokenParam,
|
|
301
387
|
}),
|
|
302
|
-
}, async ({ calendarId, account }) => {
|
|
388
|
+
}, async ({ calendarId, account, confirmToken }, ctx) => {
|
|
389
|
+
const got = await runOrDiagnose(
|
|
390
|
+
['api', 'call', 'calendar', 'v3', 'calendars.get', `--params=${JSON.stringify({ calendarId })}`], { account });
|
|
391
|
+
if (got.isError) return got;
|
|
392
|
+
const { etag, ...calendar } = calendarSnapshot(resultText(got), calendarId);
|
|
393
|
+
const view = { calendar, deletes: 'the calendar and every event on it, for good' };
|
|
394
|
+
const confirmation = await requireDispatchConfirmation(ctx, {
|
|
395
|
+
action: 'calendar.delete-calendar',
|
|
396
|
+
message: 'Review and confirm PERMANENTLY deleting this calendar and all of its events:',
|
|
397
|
+
confirmationLabel: 'Confirm that this calendar should be deleted now.',
|
|
398
|
+
details: view,
|
|
399
|
+
unsupportedNote: 'Ask the user to delete it from Google Calendar\'s settings, or use gog_calendar_unsubscribe to only hide it.',
|
|
400
|
+
fallback: {
|
|
401
|
+
tool: 'gog_calendar_delete_calendar',
|
|
402
|
+
account,
|
|
403
|
+
confirmToken,
|
|
404
|
+
subject: () => ({ target: calendarId, revision: etag, payload: view, preview: view }),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
if (confirmation) return confirmation;
|
|
303
408
|
return runOrDiagnose(['calendar', 'delete-calendar', pos(calendarId), '--force'], { account }); // gog gates this op; without --force the runner's --no-input makes it refuse
|
|
304
409
|
});
|
|
305
410
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { registerExtraCalendarTools, calendarSnapshot } from '../../src/tools/calendar-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
|
+
// ============================================================================
|
|
16
|
+
// Fleet audit 2026-09-24 SEC-6: a move that emails the guests, an Out of Office
|
|
17
|
+
// block that auto-declines (and notifies) every conflicting organizer, and a
|
|
18
|
+
// calendar delete all ask first — prompted, refused, and via the token fallback.
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
process.env = { ...ORIGINAL_ENV };
|
|
25
|
+
delete process.env.MCP_CONFIRM_MODE;
|
|
26
|
+
process.env.GOG_ACCOUNT = 'me@example.com';
|
|
27
|
+
resetConfirmTokenState();
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => { process.env = ORIGINAL_ENV; });
|
|
30
|
+
|
|
31
|
+
const json = (r: { content: Array<{ text?: string }> }) => JSON.parse(r.content[0]!.text as string);
|
|
32
|
+
|
|
33
|
+
async function prompted(answer: ElicitResult = { action: 'accept', content: { confirmed: true } }) {
|
|
34
|
+
const seen: ElicitRequest[] = [];
|
|
35
|
+
const harness = await createTestHarness(registerExtraCalendarTools, { elicitation: async (r) => { seen.push(r); return answer; } });
|
|
36
|
+
const details = () => JSON.parse(seen[0]!.params.message.split('\n').slice(1).join('\n')).details;
|
|
37
|
+
return { harness, seen, details };
|
|
38
|
+
}
|
|
39
|
+
const unprompted = () => createTestHarness(registerExtraCalendarTools);
|
|
40
|
+
|
|
41
|
+
/** Answer reads keyed by their command words; a `failing` prefix returns an error result. */
|
|
42
|
+
function stub(reads: Record<string, string>, failing?: string) {
|
|
43
|
+
vi.mocked(lib.runOrDiagnose).mockImplementation(async (args) => {
|
|
44
|
+
const key = args.filter((a): a is string => typeof a === 'string' && !a.startsWith('--')).join(' ');
|
|
45
|
+
if (failing && key.startsWith(failing)) return { content: [{ type: 'text', text: 'Error: not found' }], isError: true };
|
|
46
|
+
for (const [prefix, out] of Object.entries(reads)) if (key.startsWith(prefix)) return rawTextResult(out);
|
|
47
|
+
return rawTextResult('{"ok":true}');
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const callsTo = (...words: string[]) => vi.mocked(lib.runOrDiagnose).mock.calls.filter(([args]) =>
|
|
51
|
+
words.every((w, i) => args[i] === w));
|
|
52
|
+
|
|
53
|
+
describe('gog_calendar_move', () => {
|
|
54
|
+
const EVENT = (etag = '"v1"') => JSON.stringify({ event: {
|
|
55
|
+
id: 'e1', etag, summary: 'Budget review', start: { dateTime: '2026-09-30T14:00:00-04:00' },
|
|
56
|
+
end: { dateTime: '2026-09-30T15:00:00-04:00' }, organizer: { email: 'me@example.com' },
|
|
57
|
+
attendees: [{ email: 'me@example.com', self: true }, { email: 'boss@example.com' }],
|
|
58
|
+
} });
|
|
59
|
+
const ARGS = { calendarId: 'primary', eventId: 'e1', destinationCalendarId: 'team@example.com', sendUpdates: 'all' as const };
|
|
60
|
+
|
|
61
|
+
it.each([undefined, 'none'] as const)('moves without reading or asking when sendUpdates is %s', async (sendUpdates) => {
|
|
62
|
+
stub({});
|
|
63
|
+
const { harness, seen } = await prompted();
|
|
64
|
+
await harness.callTool('gog_calendar_move', { ...ARGS, sendUpdates });
|
|
65
|
+
expect(seen).toHaveLength(0);
|
|
66
|
+
expect(callsTo('calendar', 'event')).toHaveLength(0);
|
|
67
|
+
expect(callsTo('calendar', 'move')).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reads the event and asks, naming it, its guests and who is emailed', async () => {
|
|
71
|
+
stub({ 'calendar event': EVENT() });
|
|
72
|
+
const { harness, details } = await prompted();
|
|
73
|
+
await harness.callTool('gog_calendar_move', { ...ARGS, sendUpdates: 'externalOnly' });
|
|
74
|
+
expect(details()).toEqual({
|
|
75
|
+
calendarId: 'primary', eventId: 'e1', destinationCalendarId: 'team@example.com',
|
|
76
|
+
event: { summary: 'Budget review', start: '2026-09-30T14:00:00-04:00', end: '2026-09-30T15:00:00-04:00', organizer: 'me@example.com', guests: ['boss@example.com'] },
|
|
77
|
+
emails: 'guests outside your domain',
|
|
78
|
+
});
|
|
79
|
+
expect(callsTo('calendar', 'move')[0]![0]).toEqual(
|
|
80
|
+
['calendar', 'move', pos('primary'), pos('e1'), pos('team@example.com'), '--send-updates=externalOnly']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('moves nothing when the user declines, or when the read fails', async () => {
|
|
84
|
+
stub({ 'calendar event': EVENT() });
|
|
85
|
+
const declined = await prompted({ action: 'decline' });
|
|
86
|
+
expect(json(await declined.harness.callTool('gog_calendar_move', ARGS))).toMatchObject({ cancelled: true, action: 'calendar.move' });
|
|
87
|
+
stub({}, 'calendar event');
|
|
88
|
+
const { harness, seen } = await prompted();
|
|
89
|
+
expect((await harness.callTool('gog_calendar_move', ARGS)).isError).toBe(true);
|
|
90
|
+
expect(seen).toHaveLength(0);
|
|
91
|
+
expect(callsTo('calendar', 'move')).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('refuses a client that cannot be prompted, pointing at sendUpdates none', async () => {
|
|
95
|
+
process.env.MCP_CONFIRM_MODE = 'refuse';
|
|
96
|
+
stub({ 'calendar event': EVENT() });
|
|
97
|
+
const r = json(await (await unprompted()).callTool('gog_calendar_move', ARGS));
|
|
98
|
+
expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: 'calendar.move' });
|
|
99
|
+
expect(r.note).toMatch(/sendUpdates none/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('token fallback: an event edited between the phases (etag rotated) is DRAFT_CHANGED', async () => {
|
|
103
|
+
process.env.MCP_CONFIRM_MODE = 'ask-user';
|
|
104
|
+
stub({ 'calendar event': EVENT() });
|
|
105
|
+
const harness = await unprompted();
|
|
106
|
+
const p1 = json(await harness.callTool('gog_calendar_move', ARGS));
|
|
107
|
+
expect(p1.preview).toMatchObject({ event: { summary: 'Budget review', guests: ['boss@example.com'] }, emails: 'every guest' });
|
|
108
|
+
stub({ 'calendar event': EVENT('"v2"') });
|
|
109
|
+
expect(json(await harness.callTool('gog_calendar_move', { ...ARGS, confirmToken: p1.confirmToken })))
|
|
110
|
+
.toMatchObject({ error: 'DRAFT_CHANGED' });
|
|
111
|
+
stub({ 'calendar event': EVENT() });
|
|
112
|
+
await harness.callTool('gog_calendar_move', { ...ARGS, confirmToken: p1.confirmToken });
|
|
113
|
+
expect(callsTo('calendar', 'move')).toHaveLength(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('gog_calendar_out_of_office', () => {
|
|
118
|
+
const ARGS = { from: '2026-10-01', to: '2026-10-05' };
|
|
119
|
+
|
|
120
|
+
it('creates a block that declines nothing without asking', async () => {
|
|
121
|
+
stub({});
|
|
122
|
+
const { harness, seen } = await prompted();
|
|
123
|
+
await harness.callTool('gog_calendar_out_of_office', { ...ARGS, autoDecline: 'none' });
|
|
124
|
+
expect(seen).toHaveLength(0);
|
|
125
|
+
expect(callsTo('calendar', 'out-of-office')).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("asks by default, because gog's default declines every conflicting meeting", async () => {
|
|
129
|
+
stub({});
|
|
130
|
+
const { harness, details } = await prompted();
|
|
131
|
+
await harness.callTool('gog_calendar_out_of_office', ARGS);
|
|
132
|
+
expect(details()).toEqual({
|
|
133
|
+
calendarId: 'primary', from: '2026-10-01', to: '2026-10-05', allDay: false,
|
|
134
|
+
summary: 'Out of office', declines: 'every existing and new conflicting meeting',
|
|
135
|
+
});
|
|
136
|
+
expect(callsTo('calendar', 'out-of-office')).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('asks for new-only declines too, showing the message', async () => {
|
|
140
|
+
stub({});
|
|
141
|
+
const { harness, details } = await prompted({ action: 'decline' });
|
|
142
|
+
await harness.callTool('gog_calendar_out_of_office', { ...ARGS, calendarId: 'work', summary: 'Leave', autoDecline: 'new', declineMessage: 'Back Monday', allDay: true });
|
|
143
|
+
expect(details()).toEqual({
|
|
144
|
+
calendarId: 'work', from: '2026-10-01', to: '2026-10-05', allDay: true,
|
|
145
|
+
summary: 'Leave', declines: 'new conflicting invitations', declineMessage: 'Back Monday',
|
|
146
|
+
});
|
|
147
|
+
expect(callsTo('calendar', 'out-of-office')).toHaveLength(0);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('refuses a client that cannot be prompted, pointing at autoDecline none', async () => {
|
|
151
|
+
process.env.MCP_CONFIRM_MODE = 'refuse';
|
|
152
|
+
const r = json(await (await unprompted()).callTool('gog_calendar_out_of_office', ARGS));
|
|
153
|
+
expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: 'calendar.out-of-office' });
|
|
154
|
+
expect(r.note).toMatch(/autoDecline none/);
|
|
155
|
+
expect(lib.runOrDiagnose).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('token fallback: a changed message is DRAFT_CHANGED; the same call creates', async () => {
|
|
159
|
+
process.env.MCP_CONFIRM_MODE = 'ask-user';
|
|
160
|
+
stub({});
|
|
161
|
+
const harness = await unprompted();
|
|
162
|
+
const args = { ...ARGS, declineMessage: 'Away' };
|
|
163
|
+
const p1 = json(await harness.callTool('gog_calendar_out_of_office', args));
|
|
164
|
+
expect(p1.status).toBe('confirmation-required');
|
|
165
|
+
expect(json(await harness.callTool('gog_calendar_out_of_office', { ...args, declineMessage: 'Gone', confirmToken: p1.confirmToken })))
|
|
166
|
+
.toMatchObject({ error: 'DRAFT_CHANGED' });
|
|
167
|
+
await harness.callTool('gog_calendar_out_of_office', { ...args, confirmToken: p1.confirmToken });
|
|
168
|
+
expect(callsTo('calendar', 'out-of-office')).toHaveLength(1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('gog_calendar_delete_calendar', () => {
|
|
173
|
+
const CAL = (etag = '"c1"') => JSON.stringify({ id: 'cal1', summary: 'Team offsite', etag });
|
|
174
|
+
|
|
175
|
+
it('reads the calendar and asks by name', async () => {
|
|
176
|
+
stub({ 'api call calendar': CAL() });
|
|
177
|
+
const { harness, details, seen } = await prompted();
|
|
178
|
+
await harness.callTool('gog_calendar_delete_calendar', { calendarId: 'cal1' });
|
|
179
|
+
expect(seen[0]!.params.message).toMatch(/PERMANENTLY/);
|
|
180
|
+
expect(details()).toEqual({ calendar: { id: 'cal1', summary: 'Team offsite' }, deletes: 'the calendar and every event on it, for good' });
|
|
181
|
+
expect(callsTo('api', 'call')[0]![0]).toEqual(['api', 'call', 'calendar', 'v3', 'calendars.get', '--params={"calendarId":"cal1"}']);
|
|
182
|
+
expect(callsTo('calendar', 'delete-calendar')).toHaveLength(1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('deletes nothing when the user declines, or when the read fails', async () => {
|
|
186
|
+
stub({ 'api call calendar': CAL() });
|
|
187
|
+
await (await prompted({ action: 'decline' })).harness.callTool('gog_calendar_delete_calendar', { calendarId: 'cal1' });
|
|
188
|
+
stub({}, 'api call calendar');
|
|
189
|
+
expect((await (await prompted()).harness.callTool('gog_calendar_delete_calendar', { calendarId: 'cal1' })).isError).toBe(true);
|
|
190
|
+
expect(callsTo('calendar', 'delete-calendar')).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('refuses a client that cannot be prompted', async () => {
|
|
194
|
+
process.env.MCP_CONFIRM_MODE = 'refuse';
|
|
195
|
+
stub({ 'api call calendar': CAL() });
|
|
196
|
+
const r = json(await (await unprompted()).callTool('gog_calendar_delete_calendar', { calendarId: 'cal1' }));
|
|
197
|
+
expect(r).toMatchObject({ reason: 'confirmation-unsupported', action: 'calendar.delete-calendar' });
|
|
198
|
+
expect(r.note).toMatch(/gog_calendar_unsubscribe/);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('token fallback: a calendar changed between the phases (etag rotated) is DRAFT_CHANGED', async () => {
|
|
202
|
+
process.env.MCP_CONFIRM_MODE = 'ask-user';
|
|
203
|
+
stub({ 'api call calendar': CAL() });
|
|
204
|
+
const harness = await unprompted();
|
|
205
|
+
const p1 = json(await harness.callTool('gog_calendar_delete_calendar', { calendarId: 'cal1' }));
|
|
206
|
+
expect(p1.preview.calendar).toEqual({ id: 'cal1', summary: 'Team offsite' });
|
|
207
|
+
stub({ 'api call calendar': CAL('"c2"') });
|
|
208
|
+
expect(json(await harness.callTool('gog_calendar_delete_calendar', { calendarId: 'cal1', confirmToken: p1.confirmToken })))
|
|
209
|
+
.toMatchObject({ error: 'DRAFT_CHANGED' });
|
|
210
|
+
stub({ 'api call calendar': CAL() });
|
|
211
|
+
await harness.callTool('gog_calendar_delete_calendar', { calendarId: 'cal1', confirmToken: p1.confirmToken });
|
|
212
|
+
expect(callsTo('calendar', 'delete-calendar')).toHaveLength(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('calendarSnapshot reads a bare or result-wrapped calendar and tolerates unreadable output', () => {
|
|
216
|
+
expect(calendarSnapshot(JSON.stringify({ result: { summary: 'X', etag: 'e' } }), 'c')).toEqual({ id: 'c', summary: 'X', etag: 'e' });
|
|
217
|
+
expect(calendarSnapshot('{"summary":7}', 'c')).toEqual({ id: 'c' });
|
|
218
|
+
expect(calendarSnapshot('null', 'c')).toEqual({ id: 'c' });
|
|
219
|
+
expect(calendarSnapshot('nope', 'c')).toEqual({ id: 'c' });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -18,7 +18,11 @@ let harness: TestHarness;
|
|
|
18
18
|
beforeEach(async () => {
|
|
19
19
|
vi.clearAllMocks();
|
|
20
20
|
vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
|
|
21
|
-
|
|
21
|
+
// The gated tools (move with sendUpdates, out-of-office, delete-calendar) are
|
|
22
|
+
// exercised prompt-by-prompt in calendar-extra-gates.test.ts; here the user accepts.
|
|
23
|
+
harness = await createTestHarness(registerExtraCalendarTools, {
|
|
24
|
+
elicitation: async () => ({ action: 'accept', content: { confirmed: true } }),
|
|
25
|
+
});
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
describe('gog_meet_create', () => {
|