gogcli-mcp 2.25.0 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -5
- package/SKILL.md +11 -6
- package/dist/index.js +553 -128
- package/dist/lib.js +480 -89
- package/manifest.json +89 -1
- package/mint.yaml +106 -0
- package/package.json +5 -5
- package/server.json +2 -2
- package/src/gmail-results.ts +29 -2
- package/src/lib.ts +8 -0
- package/src/runner.ts +1 -1
- package/src/server.ts +6 -0
- package/src/tools/appscript.ts +173 -0
- package/src/tools/calendar.ts +56 -2
- package/src/tools/chat.ts +253 -0
- package/src/tools/gmail.ts +136 -5
- package/src/tools/utils.ts +20 -0
- package/src/worker.ts +1 -1
- package/tests/gmail-results.test.ts +47 -0
- package/tests/server.test.ts +61 -0
- package/tests/tools/appscript.test.ts +159 -0
- package/tests/tools/calendar.test.ts +121 -0
- package/tests/tools/chat.test.ts +284 -0
- package/tests/tools/gmail.test.ts +320 -0
package/dist/lib.js
CHANGED
|
@@ -23042,7 +23042,7 @@ function activeExecutor() {
|
|
|
23042
23042
|
return runExecutor.getStore() ?? defaultExecutor;
|
|
23043
23043
|
}
|
|
23044
23044
|
var TIMEOUT_MS = 3e4;
|
|
23045
|
-
var MIN_GOG_VERSION = "0.
|
|
23045
|
+
var MIN_GOG_VERSION = "0.38.1";
|
|
23046
23046
|
function readonlyEnvEnabled() {
|
|
23047
23047
|
return readEnvVar("GOG_READONLY") !== void 0 && parseBoolEnv("GOG_READONLY", { default: true });
|
|
23048
23048
|
}
|
|
@@ -23681,6 +23681,13 @@ function formatAuthHealth(raw, now) {
|
|
|
23681
23681
|
}
|
|
23682
23682
|
return accounts.map((a) => formatOneAccountHealth(a, now)).join("\n\n");
|
|
23683
23683
|
}
|
|
23684
|
+
function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
|
|
23685
|
+
if (inlineValue !== void 0 && fileValue !== void 0) {
|
|
23686
|
+
throw new Error(
|
|
23687
|
+
`${inlineParam} and ${fileParam} are mutually exclusive \u2014 gog accepts only one of them. Pass ${inlineParam} with the content itself (it is written to a temp file automatically when large), or ${fileParam} with a path that already exists on the gog server.`
|
|
23688
|
+
);
|
|
23689
|
+
}
|
|
23690
|
+
}
|
|
23684
23691
|
|
|
23685
23692
|
// src/tools/api.ts
|
|
23686
23693
|
function registerApiTools(server) {
|
|
@@ -23736,6 +23743,117 @@ function registerApiTools(server) {
|
|
|
23736
23743
|
});
|
|
23737
23744
|
}
|
|
23738
23745
|
|
|
23746
|
+
// src/tools/appscript.ts
|
|
23747
|
+
function registerAppScriptTools(server) {
|
|
23748
|
+
const scriptIdParam = external_exports.string().describe(
|
|
23749
|
+
"Apps Script project ID \u2014 the long ID in script.google.com/\u2026/projects/<scriptId>/\u2026, NOT the Drive file ID of a container document"
|
|
23750
|
+
);
|
|
23751
|
+
const apiEnableNote = " Needs the Apps Script API enabled on the OAuth client's Google Cloud project; if it is not, gog says so and prints the console URL to enable it. That is a project setting, not a missing scope \u2014 re-authorizing will not fix it.";
|
|
23752
|
+
server.registerTool("gog_appscript_get", {
|
|
23753
|
+
description: "Get an Apps Script project's metadata: title, creator, create/update times, and the parent Drive file when the project is bound to a Sheet, Doc or Form. Use gog_appscript_content to read the actual code." + apiEnableNote,
|
|
23754
|
+
annotations: { readOnlyHint: true },
|
|
23755
|
+
inputSchema: {
|
|
23756
|
+
scriptId: scriptIdParam,
|
|
23757
|
+
account: accountParam
|
|
23758
|
+
}
|
|
23759
|
+
}, async ({ scriptId, account }) => {
|
|
23760
|
+
return runOrDiagnose(["appscript", "get", scriptId], { account });
|
|
23761
|
+
});
|
|
23762
|
+
server.registerTool("gog_appscript_content", {
|
|
23763
|
+
description: `Read a project's source \u2014 every .gs file and its appsscript.json manifest \u2014 INLINE in the response. This is the tool to reach for when the question is "what does this script do"; it needs no filesystem, so it works the same on a hosted deployment as it does locally, unlike gog_appscript_pull.` + apiEnableNote,
|
|
23764
|
+
annotations: { readOnlyHint: true },
|
|
23765
|
+
inputSchema: {
|
|
23766
|
+
scriptId: scriptIdParam,
|
|
23767
|
+
account: accountParam
|
|
23768
|
+
}
|
|
23769
|
+
}, async ({ scriptId, account }) => {
|
|
23770
|
+
return runOrDiagnose(["appscript", "content", scriptId], { account });
|
|
23771
|
+
});
|
|
23772
|
+
server.registerTool("gog_appscript_pull", {
|
|
23773
|
+
description: "Write a project's files into a local directory, for editing a script as ordinary files. THE DIRECTORY IS RESOLVED WHERE GOG RUNS, which is the caller's own machine only on a local (stdio) deployment: on the hosted connector, or any GOG_RUNNER_URL backend, the files land on that server where the caller cannot reach them. Use gog_appscript_content there instead \u2014 it returns the same source in the response. Existing files are left alone unless overwrite is set. Read-only as far as Google is concerned: nothing is pushed back." + apiEnableNote,
|
|
23774
|
+
inputSchema: {
|
|
23775
|
+
scriptId: scriptIdParam,
|
|
23776
|
+
dir: external_exports.string().describe("Destination directory, resolved on the machine where gog runs"),
|
|
23777
|
+
overwrite: external_exports.boolean().optional().describe("Overwrite files that already exist in dir"),
|
|
23778
|
+
account: accountParam
|
|
23779
|
+
}
|
|
23780
|
+
}, async ({ scriptId, dir, overwrite, account }) => {
|
|
23781
|
+
const args = ["appscript", "pull", scriptId, dir];
|
|
23782
|
+
if (overwrite) args.push("--overwrite");
|
|
23783
|
+
return runOrDiagnose(args, { account });
|
|
23784
|
+
});
|
|
23785
|
+
server.registerTool("gog_appscript_create", {
|
|
23786
|
+
description: "Create a new, empty Apps Script project. Pass parentId to bind it to a Drive file (a Sheet, Doc or Form), which is what makes the script a container-bound script with access to that document; omit it for a standalone project. gog cannot upload code, so the project starts empty either way." + apiEnableNote,
|
|
23787
|
+
inputSchema: {
|
|
23788
|
+
title: external_exports.string().describe("Project title"),
|
|
23789
|
+
parentId: external_exports.string().optional().describe("Drive file ID to bind the project to (Sheet, Doc or Form). Omit for a standalone project."),
|
|
23790
|
+
account: accountParam
|
|
23791
|
+
}
|
|
23792
|
+
}, async ({ title, parentId, account }) => {
|
|
23793
|
+
const args = ["appscript", "create", `--title=${title}`];
|
|
23794
|
+
if (parentId) args.push(`--parent-id=${parentId}`);
|
|
23795
|
+
return runOrDiagnose(args, { account });
|
|
23796
|
+
});
|
|
23797
|
+
server.registerTool("gog_appscript_deployments", {
|
|
23798
|
+
description: "List a project's deployments \u2014 the published web apps, add-ons and API executables, each pinned to a version. A deployment ID from here is what gog_appscript_run_function needs when a script is not running in dev mode." + apiEnableNote,
|
|
23799
|
+
annotations: { readOnlyHint: true },
|
|
23800
|
+
inputSchema: {
|
|
23801
|
+
scriptId: scriptIdParam,
|
|
23802
|
+
...paginationParams,
|
|
23803
|
+
account: accountParam
|
|
23804
|
+
}
|
|
23805
|
+
}, async ({ scriptId, max, pageToken, page, all, account }) => {
|
|
23806
|
+
const args = ["appscript", "deployments", scriptId];
|
|
23807
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
23808
|
+
return runOrDiagnose(args, { account });
|
|
23809
|
+
});
|
|
23810
|
+
server.registerTool("gog_appscript_versions", {
|
|
23811
|
+
description: `List a project's saved versions \u2014 the immutable snapshots deployments point at, with their numbers and descriptions. Useful for answering "what is actually deployed" next to gog_appscript_deployments.` + apiEnableNote,
|
|
23812
|
+
annotations: { readOnlyHint: true },
|
|
23813
|
+
inputSchema: {
|
|
23814
|
+
scriptId: scriptIdParam,
|
|
23815
|
+
...paginationParams,
|
|
23816
|
+
account: accountParam
|
|
23817
|
+
}
|
|
23818
|
+
}, async ({ scriptId, max, pageToken, page, all, account }) => {
|
|
23819
|
+
const args = ["appscript", "versions", scriptId];
|
|
23820
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
23821
|
+
return runOrDiagnose(args, { account });
|
|
23822
|
+
});
|
|
23823
|
+
server.registerTool("gog_appscript_run_function", {
|
|
23824
|
+
description: "Execute a function in a deployed Apps Script project. TREAT THIS AS ARBITRARY CODE EXECUTION: the script runs with this Google account's authority and can send mail, edit Drive files or call external services, and the wrapper cannot tell a read from a write \u2014 read the code with gog_appscript_content first if you did not write it. Requires the project to be deployed as an API executable and to share the OAuth client with the calling credentials, otherwise Google refuses regardless of scopes. devMode runs the latest saved code instead of the deployed version, and only works if the account owns the script. This is NOT the escape hatch \u2014 gog_appscript_run is that." + apiEnableNote,
|
|
23825
|
+
annotations: { destructiveHint: true },
|
|
23826
|
+
inputSchema: {
|
|
23827
|
+
scriptId: scriptIdParam,
|
|
23828
|
+
functionName: external_exports.string().describe('Name of the function to call, e.g. "doWork"'),
|
|
23829
|
+
params: external_exports.string().optional().describe(`Function parameters as a JSON ARRAY of positional arguments, e.g. '["a", 1]' \u2014 not an object`),
|
|
23830
|
+
devMode: external_exports.boolean().optional().describe("Run the latest saved code rather than the deployed version (owner only)"),
|
|
23831
|
+
account: accountParam
|
|
23832
|
+
}
|
|
23833
|
+
}, async ({ scriptId, functionName, params, devMode, account }) => {
|
|
23834
|
+
if (params !== void 0) {
|
|
23835
|
+
let parsed;
|
|
23836
|
+
try {
|
|
23837
|
+
parsed = JSON.parse(params);
|
|
23838
|
+
} catch {
|
|
23839
|
+
throw new Error(`params must be a JSON array of positional arguments, e.g. '["a", 1]'. Received: ${params}`);
|
|
23840
|
+
}
|
|
23841
|
+
if (!Array.isArray(parsed)) {
|
|
23842
|
+
throw new Error(`params must be a JSON ARRAY of positional arguments, e.g. '["a", 1]' \u2014 Apps Script takes positional arguments, not named ones. Received: ${params}`);
|
|
23843
|
+
}
|
|
23844
|
+
}
|
|
23845
|
+
const args = ["appscript", "run", scriptId, functionName];
|
|
23846
|
+
if (params !== void 0) args.push(`--params=${params}`);
|
|
23847
|
+
if (devMode) args.push("--dev-mode");
|
|
23848
|
+
return runOrDiagnose(args, { account });
|
|
23849
|
+
});
|
|
23850
|
+
registerRunTool(server, {
|
|
23851
|
+
service: "appscript",
|
|
23852
|
+
examples: '"get", "content", "deployments"',
|
|
23853
|
+
note: "To execute a function, use gog_appscript_run_function \u2014 this tool is the generic escape hatch."
|
|
23854
|
+
});
|
|
23855
|
+
}
|
|
23856
|
+
|
|
23739
23857
|
// src/tools/auth.ts
|
|
23740
23858
|
function registerAuthToolsWith(server, defaultServices) {
|
|
23741
23859
|
const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
|
|
@@ -23871,6 +23989,29 @@ function authToolsFor(defaultServices) {
|
|
|
23871
23989
|
}
|
|
23872
23990
|
|
|
23873
23991
|
// src/tools/calendar.ts
|
|
23992
|
+
var reminderParams = {
|
|
23993
|
+
reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
|
|
23994
|
+
`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.`
|
|
23995
|
+
),
|
|
23996
|
+
noReminders: external_exports.boolean().optional().describe(
|
|
23997
|
+
"Give the event no reminders at all, overriding the calendar's defaults. Different from an empty reminders array, which RESTORES those defaults. Cannot be combined with reminders."
|
|
23998
|
+
)
|
|
23999
|
+
};
|
|
24000
|
+
function pushReminderFlags(args, p) {
|
|
24001
|
+
if (p.noReminders) {
|
|
24002
|
+
if (p.reminders !== void 0) {
|
|
24003
|
+
throw new Error("reminders and noReminders are mutually exclusive: pass reminders to set custom ones, noReminders for none, or an empty reminders array to restore the calendar defaults.");
|
|
24004
|
+
}
|
|
24005
|
+
args.push("--no-reminders");
|
|
24006
|
+
return;
|
|
24007
|
+
}
|
|
24008
|
+
if (p.reminders === void 0) return;
|
|
24009
|
+
if (p.reminders.length === 0) {
|
|
24010
|
+
args.push("--reminder=");
|
|
24011
|
+
return;
|
|
24012
|
+
}
|
|
24013
|
+
for (const reminder of p.reminders) args.push(`--reminder=${reminder}`);
|
|
24014
|
+
}
|
|
23874
24015
|
function registerCalendarTools(server) {
|
|
23875
24016
|
server.registerTool("gog_calendar_events", {
|
|
23876
24017
|
description: 'List calendar events. Describe the window ONE way and one way only (gog >= 0.36.0 rejects the rest as ambiguous rather than silently discarding a flag): today on its own; or from + to; or from + days; or days on its own (a window of that many days starting today). today cannot be combined with from, to or days, and days cannot be combined with to. gog returns only 10 events by default, so a wide date range is USUALLY INCOMPLETE: raise max, or page with pageToken until the response carries no nextPageToken. A response carrying "truncated": true is an incomplete view \u2014 never conclude an event does not exist from one.',
|
|
@@ -23940,9 +24081,10 @@ function registerCalendarTools(server) {
|
|
|
23940
24081
|
allDay: external_exports.boolean().optional().describe("All-day event (use date-only in from/to)"),
|
|
23941
24082
|
timezone: external_exports.string().optional().describe("IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden."),
|
|
23942
24083
|
withZoom: external_exports.boolean().optional().describe("Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)"),
|
|
24084
|
+
...reminderParams,
|
|
23943
24085
|
account: accountParam
|
|
23944
24086
|
}
|
|
23945
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, account }) => {
|
|
24087
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, reminders, noReminders, account }) => {
|
|
23946
24088
|
const args = ["calendar", "create", calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
23947
24089
|
if (description) args.push(`--description=${description}`);
|
|
23948
24090
|
if (location) args.push(`--location=${location}`);
|
|
@@ -23950,6 +24092,7 @@ function registerCalendarTools(server) {
|
|
|
23950
24092
|
if (allDay) args.push("--all-day");
|
|
23951
24093
|
if (timezone) args.push(`--timezone=${timezone}`);
|
|
23952
24094
|
if (withZoom) args.push("--with-zoom");
|
|
24095
|
+
pushReminderFlags(args, { reminders, noReminders });
|
|
23953
24096
|
return runOrDiagnose(args, { account });
|
|
23954
24097
|
});
|
|
23955
24098
|
server.registerTool("gog_calendar_update", {
|
|
@@ -23970,9 +24113,10 @@ function registerCalendarTools(server) {
|
|
|
23970
24113
|
regenerateZoom: external_exports.boolean().optional().describe("Replace the event's existing Zoom video conference"),
|
|
23971
24114
|
removeZoom: external_exports.boolean().optional().describe("Remove the event's Zoom video conference"),
|
|
23972
24115
|
removeMeet: external_exports.boolean().optional().describe("Remove the event's Google Meet video conference (clears conference data only)"),
|
|
24116
|
+
...reminderParams,
|
|
23973
24117
|
account: accountParam
|
|
23974
24118
|
}
|
|
23975
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, account }) => {
|
|
24119
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, reminders, noReminders, account }) => {
|
|
23976
24120
|
const args = ["calendar", "update", calendarId, eventId];
|
|
23977
24121
|
if (summary !== void 0) args.push(`--summary=${summary}`);
|
|
23978
24122
|
if (from !== void 0) args.push(`--from=${from}`);
|
|
@@ -23986,6 +24130,7 @@ function registerCalendarTools(server) {
|
|
|
23986
24130
|
if (regenerateZoom) args.push("--regenerate-zoom");
|
|
23987
24131
|
if (removeZoom) args.push("--remove-zoom");
|
|
23988
24132
|
if (removeMeet) args.push("--remove-meet");
|
|
24133
|
+
pushReminderFlags(args, { reminders, noReminders });
|
|
23989
24134
|
return runOrDiagnose(args, { account });
|
|
23990
24135
|
});
|
|
23991
24136
|
server.registerTool("gog_calendar_delete", {
|
|
@@ -24017,6 +24162,255 @@ function registerCalendarTools(server) {
|
|
|
24017
24162
|
registerRunTool(server, { service: "calendar", examples: '"calendars", "freebusy"' });
|
|
24018
24163
|
}
|
|
24019
24164
|
|
|
24165
|
+
// src/attachments.ts
|
|
24166
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
24167
|
+
var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
24168
|
+
var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
|
|
24169
|
+
var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
|
|
24170
|
+
var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
|
|
24171
|
+
function wireBytesOf(arg) {
|
|
24172
|
+
if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
|
|
24173
|
+
return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
|
|
24174
|
+
}
|
|
24175
|
+
var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
|
|
24176
|
+
var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
|
|
24177
|
+
var inlineAttachmentSchema = external_exports.object({
|
|
24178
|
+
filename: external_exports.string().min(1).describe(
|
|
24179
|
+
`Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
|
|
24180
|
+
),
|
|
24181
|
+
contentBase64: external_exports.string().min(1).describe(
|
|
24182
|
+
"The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
|
|
24183
|
+
)
|
|
24184
|
+
});
|
|
24185
|
+
var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
|
|
24186
|
+
`Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
|
|
24187
|
+
);
|
|
24188
|
+
function validateFilename(filename, where) {
|
|
24189
|
+
if (/[/\\]/.test(filename)) {
|
|
24190
|
+
throw new Error(
|
|
24191
|
+
`${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
|
|
24192
|
+
);
|
|
24193
|
+
}
|
|
24194
|
+
if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
|
|
24195
|
+
throw new Error(
|
|
24196
|
+
`${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
|
|
24197
|
+
);
|
|
24198
|
+
}
|
|
24199
|
+
}
|
|
24200
|
+
function decodedLength(contentBase64) {
|
|
24201
|
+
const buf = Buffer.from(contentBase64, "base64");
|
|
24202
|
+
return buf.toString("base64") === contentBase64 ? buf.length : null;
|
|
24203
|
+
}
|
|
24204
|
+
function inlineFileArg(flag, attachment, opts = {}) {
|
|
24205
|
+
const { filename, contentBase64 } = attachment;
|
|
24206
|
+
const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
|
|
24207
|
+
validateFilename(filename, where);
|
|
24208
|
+
const bytes = decodedLength(contentBase64);
|
|
24209
|
+
if (bytes === null) {
|
|
24210
|
+
throw new Error(
|
|
24211
|
+
`${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
|
|
24212
|
+
);
|
|
24213
|
+
}
|
|
24214
|
+
if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
24215
|
+
throw new Error(
|
|
24216
|
+
`${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
|
|
24217
|
+
);
|
|
24218
|
+
}
|
|
24219
|
+
const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
|
|
24220
|
+
if (opts.positional) arg.positional = true;
|
|
24221
|
+
return { arg, bytes };
|
|
24222
|
+
}
|
|
24223
|
+
function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
24224
|
+
if (!attachments?.length) return [];
|
|
24225
|
+
const args = [];
|
|
24226
|
+
const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
|
|
24227
|
+
let attachmentWire = 0;
|
|
24228
|
+
let decodedTotal = 0;
|
|
24229
|
+
for (const attachment of attachments) {
|
|
24230
|
+
const { arg, bytes } = inlineFileArg(flag, attachment);
|
|
24231
|
+
attachmentWire += arg.contents.length;
|
|
24232
|
+
decodedTotal += bytes;
|
|
24233
|
+
if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
|
|
24234
|
+
const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
|
|
24235
|
+
throw new Error(
|
|
24236
|
+
`This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
|
|
24237
|
+
);
|
|
24238
|
+
}
|
|
24239
|
+
args.push(arg);
|
|
24240
|
+
}
|
|
24241
|
+
return args;
|
|
24242
|
+
}
|
|
24243
|
+
|
|
24244
|
+
// src/tools/chat.ts
|
|
24245
|
+
function registerChatTools(server) {
|
|
24246
|
+
const workspaceOnlyNote = ' WORKSPACE ONLY: Google Chat has no API for consumer accounts, so this fails on an @gmail.com account with "chat requires a Google Workspace account". That is the ACCOUNT, not the token \u2014 re-authorizing or adding scopes will not help.';
|
|
24247
|
+
const spaceParam = external_exports.string().describe(
|
|
24248
|
+
'Space resource name, e.g. "spaces/AAAAsomeID" (from gog_chat_spaces_list, gog_chat_spaces_find or gog_chat_dm_space)'
|
|
24249
|
+
);
|
|
24250
|
+
const threadParam = external_exports.string().optional().describe(
|
|
24251
|
+
'Thread resource name, e.g. "spaces/AAAA/threads/CCCC" \u2014 reply inside that thread instead of starting a new one'
|
|
24252
|
+
);
|
|
24253
|
+
server.registerTool("gog_chat_spaces_list", {
|
|
24254
|
+
description: "List the Google Chat spaces the account belongs to \u2014 named rooms and DMs alike \u2014 with their resource names. Start here when you do not yet have a space name; gog_chat_spaces_find is faster when you know the room's title." + workspaceOnlyNote,
|
|
24255
|
+
annotations: { readOnlyHint: true },
|
|
24256
|
+
inputSchema: {
|
|
24257
|
+
...paginationParams,
|
|
24258
|
+
account: accountParam
|
|
24259
|
+
}
|
|
24260
|
+
}, async ({ max, pageToken, page, all, account }) => {
|
|
24261
|
+
const args = ["chat", "spaces", "list"];
|
|
24262
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
24263
|
+
return runOrDiagnose(args, { account });
|
|
24264
|
+
});
|
|
24265
|
+
server.registerTool("gog_chat_spaces_find", {
|
|
24266
|
+
description: 'Find spaces whose display name matches. Substring and case-insensitive by default, which is what you want when the user names a room approximately ("the launch room"); pass exact=true to require the whole title. DMs have no display name \u2014 use gog_chat_dm_space to reach a person.' + workspaceOnlyNote,
|
|
24267
|
+
annotations: { readOnlyHint: true },
|
|
24268
|
+
inputSchema: {
|
|
24269
|
+
displayName: external_exports.string().describe("Space display name, or part of one"),
|
|
24270
|
+
exact: external_exports.boolean().optional().describe("Require an exact (still case-insensitive) match on the whole display name"),
|
|
24271
|
+
max: external_exports.number().int().optional().describe("Max results per page"),
|
|
24272
|
+
account: accountParam
|
|
24273
|
+
}
|
|
24274
|
+
}, async ({ displayName, exact, max, account }) => {
|
|
24275
|
+
const args = ["chat", "spaces", "find", displayName];
|
|
24276
|
+
if (exact) args.push("--exact");
|
|
24277
|
+
if (max !== void 0) args.push(`--max=${max}`);
|
|
24278
|
+
return runOrDiagnose(args, { account });
|
|
24279
|
+
});
|
|
24280
|
+
server.registerTool("gog_chat_spaces_create", {
|
|
24281
|
+
description: "Create a named Chat space, optionally seeding its membership. Members are added immediately and are notified \u2014 this is visible to other people the moment it runs, so confirm the member list before calling it." + workspaceOnlyNote,
|
|
24282
|
+
inputSchema: {
|
|
24283
|
+
displayName: external_exports.string().describe("Display name for the new space"),
|
|
24284
|
+
members: external_exports.array(external_exports.string()).optional().describe('Initial members, as email addresses or "users/..." resource names'),
|
|
24285
|
+
account: accountParam
|
|
24286
|
+
}
|
|
24287
|
+
}, async ({ displayName, members, account }) => {
|
|
24288
|
+
const args = ["chat", "spaces", "create", displayName];
|
|
24289
|
+
if (members) for (const member of members) args.push(`--member=${member}`);
|
|
24290
|
+
return runOrDiagnose(args, { account });
|
|
24291
|
+
});
|
|
24292
|
+
server.registerTool("gog_chat_threads_list", {
|
|
24293
|
+
description: "List the threads in a space, so a reply can be targeted at an existing conversation rather than starting a new one. Pass a thread name from here as `thread` to gog_chat_messages_send." + workspaceOnlyNote,
|
|
24294
|
+
annotations: { readOnlyHint: true },
|
|
24295
|
+
inputSchema: {
|
|
24296
|
+
space: spaceParam,
|
|
24297
|
+
...paginationParams,
|
|
24298
|
+
account: accountParam
|
|
24299
|
+
}
|
|
24300
|
+
}, async ({ space, max, pageToken, page, all, account }) => {
|
|
24301
|
+
const args = ["chat", "threads", "list", space];
|
|
24302
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
24303
|
+
return runOrDiagnose(args, { account });
|
|
24304
|
+
});
|
|
24305
|
+
server.registerTool("gog_chat_messages_list", {
|
|
24306
|
+
description: `Read messages in a space. The JSON carries each message's @-mentions and a summary of its emoji reactions (gog >= 0.38.0) alongside the text, so "who was tagged" and "did anyone react" are answerable without extra calls. unread=true returns only what arrived after the account last read the space \u2014 the cheap way to answer "what did I miss". Newest-first needs an explicit order="createTime desc"; Chat's own default is oldest-first.` + workspaceOnlyNote,
|
|
24307
|
+
annotations: { readOnlyHint: true },
|
|
24308
|
+
inputSchema: {
|
|
24309
|
+
space: spaceParam,
|
|
24310
|
+
thread: threadParam,
|
|
24311
|
+
unread: external_exports.boolean().optional().describe("Only messages posted after the account last read this space"),
|
|
24312
|
+
order: external_exports.enum(["createTime asc", "createTime desc", "lastUpdateTime asc", "lastUpdateTime desc"]).optional().describe('Sort order (Chat default: "createTime asc", i.e. OLDEST first \u2014 ask for "createTime desc" when you want the latest messages)'),
|
|
24313
|
+
...paginationParams,
|
|
24314
|
+
account: accountParam
|
|
24315
|
+
}
|
|
24316
|
+
}, async ({ space, thread, unread, order, max, pageToken, page, all, account }) => {
|
|
24317
|
+
const args = ["chat", "messages", "list", space];
|
|
24318
|
+
if (thread) args.push(`--thread=${thread}`);
|
|
24319
|
+
if (unread) args.push("--unread");
|
|
24320
|
+
if (order) args.push(`--order=${order}`);
|
|
24321
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
24322
|
+
return runOrDiagnose(args, { account });
|
|
24323
|
+
});
|
|
24324
|
+
server.registerTool("gog_chat_messages_send", {
|
|
24325
|
+
description: "Post a message to a Chat space. THIS IS IMMEDIATELY VISIBLE TO EVERYONE IN THE SPACE and cannot be unsent through this tool, so treat it like sending mail, not like saving a draft. Pass `thread` to reply inside an existing conversation (from gog_chat_threads_list or a message's thread field); omit it to start a new one. Text supports Chat's markdown-ish formatting (*bold*, _italic_, `code`)." + workspaceOnlyNote,
|
|
24326
|
+
inputSchema: {
|
|
24327
|
+
space: spaceParam,
|
|
24328
|
+
text: external_exports.string().optional().describe("Message text. Optional only when an attachment is supplied."),
|
|
24329
|
+
thread: threadParam,
|
|
24330
|
+
attach: external_exports.array(external_exports.string()).optional().describe(
|
|
24331
|
+
"Attachment file paths, read WHERE GOG RUNS. On a hosted or remote deployment that is not your machine \u2014 use attachInline there instead."
|
|
24332
|
+
),
|
|
24333
|
+
attachInline: attachInlineParam,
|
|
24334
|
+
account: accountParam
|
|
24335
|
+
}
|
|
24336
|
+
}, async ({ space, text, thread, attach, attachInline, account }) => {
|
|
24337
|
+
if (text === void 0 && !attach?.length && !attachInline?.length) {
|
|
24338
|
+
throw new Error("A Chat message needs text, an attachment, or both.");
|
|
24339
|
+
}
|
|
24340
|
+
const args = ["chat", "messages", "send", space];
|
|
24341
|
+
if (text !== void 0) args.push(`--text=${text}`);
|
|
24342
|
+
if (thread) args.push(`--thread=${thread}`);
|
|
24343
|
+
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
24344
|
+
args.push(...inlineAttachmentArgs("attach", attachInline, args));
|
|
24345
|
+
return runOrDiagnose(args, { account });
|
|
24346
|
+
});
|
|
24347
|
+
server.registerTool("gog_chat_dm_send", {
|
|
24348
|
+
description: "Send a direct message to one person by email address, creating the DM space if this is the first message. Delivered immediately and cannot be unsent through this tool. For a room rather than a person, use gog_chat_messages_send." + workspaceOnlyNote,
|
|
24349
|
+
inputSchema: {
|
|
24350
|
+
email: external_exports.string().describe("Recipient email address"),
|
|
24351
|
+
text: external_exports.string().describe("Message text"),
|
|
24352
|
+
thread: threadParam,
|
|
24353
|
+
account: accountParam
|
|
24354
|
+
}
|
|
24355
|
+
}, async ({ email: email3, text, thread, account }) => {
|
|
24356
|
+
const args = ["chat", "dm", "send", email3, `--text=${text}`];
|
|
24357
|
+
if (thread) args.push(`--thread=${thread}`);
|
|
24358
|
+
return runOrDiagnose(args, { account });
|
|
24359
|
+
});
|
|
24360
|
+
server.registerTool("gog_chat_dm_space", {
|
|
24361
|
+
description: 'Resolve the DM space for an email address \u2014 the bridge from a person to the "spaces/..." name the message tools want. Creates the space if none exists yet, which is silent: it does not message the person.' + workspaceOnlyNote,
|
|
24362
|
+
inputSchema: {
|
|
24363
|
+
email: external_exports.string().describe("The other person's email address"),
|
|
24364
|
+
account: accountParam
|
|
24365
|
+
}
|
|
24366
|
+
}, async ({ email: email3, account }) => {
|
|
24367
|
+
return runOrDiagnose(["chat", "dm", "space", email3], { account });
|
|
24368
|
+
});
|
|
24369
|
+
server.registerTool("gog_chat_reactions_list", {
|
|
24370
|
+
description: "List the emoji reactions on one message, with who reacted. gog_chat_messages_list already returns a reaction SUMMARY per message; come here when you need the individual reactors, or the reaction resource names that gog_chat_reactions_delete takes." + workspaceOnlyNote,
|
|
24371
|
+
annotations: { readOnlyHint: true },
|
|
24372
|
+
inputSchema: {
|
|
24373
|
+
message: external_exports.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
|
|
24374
|
+
space: external_exports.string().optional().describe("Space resource name \u2014 required only when `message` is a bare ID"),
|
|
24375
|
+
...paginationParams,
|
|
24376
|
+
account: accountParam
|
|
24377
|
+
}
|
|
24378
|
+
}, async ({ message, space, max, pageToken, page, all, account }) => {
|
|
24379
|
+
const args = ["chat", "messages", "reactions", "list", message];
|
|
24380
|
+
if (space) args.push(`--space=${space}`);
|
|
24381
|
+
pushPaginationFlags(args, { max, pageToken, page, all });
|
|
24382
|
+
return runOrDiagnose(args, { account });
|
|
24383
|
+
});
|
|
24384
|
+
server.registerTool("gog_chat_reactions_create", {
|
|
24385
|
+
description: 'React to a message with an emoji. Visible to the space immediately. Pass the emoji itself ("\u{1F44D}"), not a :shortcode:.' + workspaceOnlyNote,
|
|
24386
|
+
inputSchema: {
|
|
24387
|
+
message: external_exports.string().describe('Message resource name ("spaces/AAAA/messages/BBBB"), or a bare message ID together with `space`'),
|
|
24388
|
+
emoji: external_exports.string().describe('The emoji character to react with, e.g. "\u{1F44D}"'),
|
|
24389
|
+
space: external_exports.string().optional().describe("Space resource name \u2014 required only when `message` is a bare ID"),
|
|
24390
|
+
account: accountParam
|
|
24391
|
+
}
|
|
24392
|
+
}, async ({ message, emoji: emoji3, space, account }) => {
|
|
24393
|
+
const args = ["chat", "messages", "reactions", "create", message, emoji3];
|
|
24394
|
+
if (space) args.push(`--space=${space}`);
|
|
24395
|
+
return runOrDiagnose(args, { account });
|
|
24396
|
+
});
|
|
24397
|
+
server.registerTool("gog_chat_reactions_delete", {
|
|
24398
|
+
description: `Remove one emoji reaction. Takes the REACTION's own resource name ("spaces/.../messages/.../reactions/..."), not the message's and not the emoji \u2014 get it from gog_chat_reactions_list. An account can only remove its own reaction.` + workspaceOnlyNote,
|
|
24399
|
+
annotations: { destructiveHint: true },
|
|
24400
|
+
inputSchema: {
|
|
24401
|
+
reaction: external_exports.string().describe('Reaction resource name, e.g. "spaces/AAAA/messages/BBBB/reactions/CCCC"'),
|
|
24402
|
+
account: accountParam
|
|
24403
|
+
}
|
|
24404
|
+
}, async ({ reaction, account }) => {
|
|
24405
|
+
return runOrDiagnose(["chat", "messages", "reactions", "delete", reaction], { account });
|
|
24406
|
+
});
|
|
24407
|
+
registerRunTool(server, {
|
|
24408
|
+
service: "chat",
|
|
24409
|
+
examples: '"spaces", "messages", "dm"',
|
|
24410
|
+
note: "Google Chat has no API for consumer accounts: every chat subcommand fails on an @gmail.com account regardless of scopes."
|
|
24411
|
+
});
|
|
24412
|
+
}
|
|
24413
|
+
|
|
24020
24414
|
// src/tools/classroom.ts
|
|
24021
24415
|
function registerClassroomTools(server) {
|
|
24022
24416
|
server.registerTool("gog_classroom_courses_list", {
|
|
@@ -24832,6 +25226,7 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
|
|
|
24832
25226
|
const merged = [];
|
|
24833
25227
|
let base;
|
|
24834
25228
|
let token = startToken;
|
|
25229
|
+
const fetched = new Set(startToken === void 0 ? [] : [startToken]);
|
|
24835
25230
|
for (let pages = 0; pages < maxPages; pages++) {
|
|
24836
25231
|
const result = await runPage(token);
|
|
24837
25232
|
const parsed = parsePage(result, itemsKey);
|
|
@@ -24840,8 +25235,17 @@ async function fetchGmailPages(runPage, itemsKey, maxPages, startToken) {
|
|
|
24840
25235
|
}
|
|
24841
25236
|
base = parsed;
|
|
24842
25237
|
merged.push(...parsed[itemsKey]);
|
|
24843
|
-
|
|
24844
|
-
if (
|
|
25238
|
+
const next = typeof parsed.nextPageToken === "string" && parsed.nextPageToken !== "" ? parsed.nextPageToken : void 0;
|
|
25239
|
+
if (next === void 0) {
|
|
25240
|
+
token = void 0;
|
|
25241
|
+
break;
|
|
25242
|
+
}
|
|
25243
|
+
if (fetched.has(next)) {
|
|
25244
|
+
token = next;
|
|
25245
|
+
break;
|
|
25246
|
+
}
|
|
25247
|
+
fetched.add(next);
|
|
25248
|
+
token = next;
|
|
24845
25249
|
}
|
|
24846
25250
|
return finish(base, itemsKey, merged, token);
|
|
24847
25251
|
}
|
|
@@ -24865,86 +25269,46 @@ function finish(base, itemsKey, merged, token) {
|
|
|
24865
25269
|
return rawTextResult(JSON.stringify(out));
|
|
24866
25270
|
}
|
|
24867
25271
|
|
|
24868
|
-
// src/attachments.ts
|
|
24869
|
-
var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
|
|
24870
|
-
var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
24871
|
-
var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
|
|
24872
|
-
var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
|
|
24873
|
-
var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
|
|
24874
|
-
function wireBytesOf(arg) {
|
|
24875
|
-
if (typeof arg === "string") return Buffer.byteLength(arg, "utf8");
|
|
24876
|
-
return arg.encoding === "base64" ? arg.contents.length : Buffer.byteLength(arg.contents, "utf8");
|
|
24877
|
-
}
|
|
24878
|
-
var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
|
|
24879
|
-
var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
|
|
24880
|
-
var inlineAttachmentSchema = external_exports.object({
|
|
24881
|
-
filename: external_exports.string().min(1).describe(
|
|
24882
|
-
`Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
|
|
24883
|
-
),
|
|
24884
|
-
contentBase64: external_exports.string().min(1).describe(
|
|
24885
|
-
"The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
|
|
24886
|
-
)
|
|
24887
|
-
});
|
|
24888
|
-
var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
|
|
24889
|
-
`Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
|
|
24890
|
-
);
|
|
24891
|
-
function validateFilename(filename, where) {
|
|
24892
|
-
if (/[/\\]/.test(filename)) {
|
|
24893
|
-
throw new Error(
|
|
24894
|
-
`${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
|
|
24895
|
-
);
|
|
24896
|
-
}
|
|
24897
|
-
if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
|
|
24898
|
-
throw new Error(
|
|
24899
|
-
`${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
|
|
24900
|
-
);
|
|
24901
|
-
}
|
|
24902
|
-
}
|
|
24903
|
-
function decodedLength(contentBase64) {
|
|
24904
|
-
const buf = Buffer.from(contentBase64, "base64");
|
|
24905
|
-
return buf.toString("base64") === contentBase64 ? buf.length : null;
|
|
24906
|
-
}
|
|
24907
|
-
function inlineFileArg(flag, attachment, opts = {}) {
|
|
24908
|
-
const { filename, contentBase64 } = attachment;
|
|
24909
|
-
const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
|
|
24910
|
-
validateFilename(filename, where);
|
|
24911
|
-
const bytes = decodedLength(contentBase64);
|
|
24912
|
-
if (bytes === null) {
|
|
24913
|
-
throw new Error(
|
|
24914
|
-
`${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
|
|
24915
|
-
);
|
|
24916
|
-
}
|
|
24917
|
-
if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
24918
|
-
throw new Error(
|
|
24919
|
-
`${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
|
|
24920
|
-
);
|
|
24921
|
-
}
|
|
24922
|
-
const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
|
|
24923
|
-
if (opts.positional) arg.positional = true;
|
|
24924
|
-
return { arg, bytes };
|
|
24925
|
-
}
|
|
24926
|
-
function inlineAttachmentArgs(flag, attachments, siblingArgs = []) {
|
|
24927
|
-
if (!attachments?.length) return [];
|
|
24928
|
-
const args = [];
|
|
24929
|
-
const siblingWire = siblingArgs.reduce((sum, arg) => sum + wireBytesOf(arg), 0);
|
|
24930
|
-
let attachmentWire = 0;
|
|
24931
|
-
let decodedTotal = 0;
|
|
24932
|
-
for (const attachment of attachments) {
|
|
24933
|
-
const { arg, bytes } = inlineFileArg(flag, attachment);
|
|
24934
|
-
attachmentWire += arg.contents.length;
|
|
24935
|
-
decodedTotal += bytes;
|
|
24936
|
-
if (siblingWire + attachmentWire > MAX_REQUEST_PAYLOAD_WIRE_BYTES) {
|
|
24937
|
-
const blame = attachmentWire <= MAX_REQUEST_PAYLOAD_WIRE_BYTES ? ` These attachments would fit on their own; the rest of the message (its body, mostly) spends ${siblingWire} bytes of the same budget.` : "";
|
|
24938
|
-
throw new Error(
|
|
24939
|
-
`This message is too large to send: ${decodedTotal} bytes of attachments (${attachmentWire} bytes once base64-encoded for transit) exceed the ${MAX_REQUEST_PAYLOAD_WIRE_BYTES}-byte request limit.${blame} The ceiling for attachments alone is ${MAX_INLINE_ATTACHMENT_TOTAL_BYTES} bytes (${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)}); a long body lowers it. Send fewer or smaller files per message, shorten the body, or upload the large files to Drive and link them.`
|
|
24940
|
-
);
|
|
24941
|
-
}
|
|
24942
|
-
args.push(arg);
|
|
24943
|
-
}
|
|
24944
|
-
return args;
|
|
24945
|
-
}
|
|
24946
|
-
|
|
24947
25272
|
// src/tools/gmail.ts
|
|
25273
|
+
var replySchema = {
|
|
25274
|
+
messageId: external_exports.string().describe("Gmail message ID to reply to \u2014 the short hex `id` from gog_gmail_get / _search (or gog_gmail_messages_search, gogcli-mcp-gmail only). NOT the threadId, NOT the RFC822 `<\u2026@host>` Message-Id header."),
|
|
25275
|
+
body: external_exports.string().optional().describe("Reply body (plain text; required unless bodyHtml or bodyHtmlFile is set). Any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body."),
|
|
25276
|
+
bodyHtml: external_exports.string().optional().describe("Reply body (HTML; optional). Pass the HTML itself at any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Mutually exclusive with bodyHtmlFile."),
|
|
25277
|
+
bodyHtmlFile: external_exports.string().optional().describe(`Path to an HTML file that ALREADY EXISTS on the gog server for the reply body. gog also accepts "-" for stdin, but this server never writes to gog's stdin, so "-" would hang until the call times out. Mutually exclusive with bodyHtml \u2014 supplying both is rejected. You rarely need this: bodyHtml handles large bodies on its own.`),
|
|
25278
|
+
to: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to To (repeatable). Added on top of the recipients inherited from the original message."),
|
|
25279
|
+
cc: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to Cc (repeatable)"),
|
|
25280
|
+
bcc: external_exports.array(external_exports.string()).optional().describe("Add or move recipients to Bcc (repeatable)"),
|
|
25281
|
+
remove: external_exports.array(external_exports.string()).optional().describe("Remove these recipients from all fields (repeatable) \u2014 e.g. to drop someone from a reply-all."),
|
|
25282
|
+
subject: external_exports.string().optional().describe('Override reply subject (default: "Re: <original>"). A changed subject starts a NEW Gmail thread.'),
|
|
25283
|
+
noQuote: external_exports.boolean().optional().describe("Do not include the original message quoted below the reply (default: the original is quoted)"),
|
|
25284
|
+
attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Read on the server, base64-encoded with a MIME type inferred from the extension.`),
|
|
25285
|
+
attachInline: attachInlineParam,
|
|
25286
|
+
from: external_exports.string().optional().describe("Send from this email address (must be a verified send-as alias)"),
|
|
25287
|
+
autoFromAddressedAlias: external_exports.boolean().optional().describe("When from is omitted, send from the verified send-as alias the original message was addressed TO, instead of the account's primary address \u2014 so a reply to mail sent to an alias goes back out from that alias. Ignored when from is set."),
|
|
25288
|
+
signature: external_exports.boolean().optional().describe("Append the Gmail signature from the active send-as address"),
|
|
25289
|
+
signatureFrom: external_exports.string().optional().describe("Append the Gmail signature from this send-as email address"),
|
|
25290
|
+
signatureFile: external_exports.string().optional().describe("Append a local signature file (plain text or HTML), read on the gog server"),
|
|
25291
|
+
account: accountParam
|
|
25292
|
+
};
|
|
25293
|
+
function appendReplyFlags(args, f) {
|
|
25294
|
+
assertNotBoth("bodyHtml", "bodyHtmlFile", f.bodyHtml, f.bodyHtmlFile);
|
|
25295
|
+
if (f.body) args.push(payloadArg("body", "body-file", f.body));
|
|
25296
|
+
if (f.bodyHtml) args.push(payloadArg("body-html", "body-html-file", f.bodyHtml, "html"));
|
|
25297
|
+
else if (f.bodyHtmlFile) args.push(`--body-html-file=${f.bodyHtmlFile}`);
|
|
25298
|
+
if (f.to) for (const r of f.to) args.push(`--to=${r}`);
|
|
25299
|
+
if (f.cc) for (const r of f.cc) args.push(`--cc=${r}`);
|
|
25300
|
+
if (f.bcc) for (const r of f.bcc) args.push(`--bcc=${r}`);
|
|
25301
|
+
if (f.remove) for (const r of f.remove) args.push(`--remove=${r}`);
|
|
25302
|
+
if (f.subject) args.push(`--subject=${f.subject}`);
|
|
25303
|
+
if (f.noQuote) args.push("--no-quote");
|
|
25304
|
+
if (f.attach) for (const p of f.attach) args.push(`--attach=${p}`);
|
|
25305
|
+
args.push(...inlineAttachmentArgs("attach", f.attachInline, args));
|
|
25306
|
+
if (f.from) args.push(`--from=${f.from}`);
|
|
25307
|
+
if (f.signature) args.push("--signature");
|
|
25308
|
+
if (f.signatureFrom) args.push(`--signature-from=${f.signatureFrom}`);
|
|
25309
|
+
if (f.signatureFile) args.push(`--signature-file=${f.signatureFile}`);
|
|
25310
|
+
args.push(f.autoFromAddressedAlias ? "--auto-from-addressed-alias" : "--auto-from-addressed-alias=false");
|
|
25311
|
+
}
|
|
24948
25312
|
function registerGmailTools(server) {
|
|
24949
25313
|
server.registerTool("gog_gmail_search", {
|
|
24950
25314
|
description: 'Search Gmail threads using Gmail query syntax (e.g. "from:alice subject:invoice is:unread"). The query is passed verbatim to Gmail; a bare name token (from:alison) matches per Gmail\'s own heuristics, a full address (from:alison@example.com) is exact. To match a contact across several addresses, OR them: from:(a@x.com OR b@y.com). Results are ALWAYS newest-first by Gmail\'s internalDate \u2014 the wrapper sorts them, so the first result is the most recent match and a recent message can never be buried below older ones. IMPORTANT \u2014 a response carrying "truncated": true is an INCOMPLETE view of the matches: NEVER report that a message does not exist, or that there is no such mail, on the strength of one. Page through it (pass nextPageToken back as `pageToken`), set maxPages to walk several pages in one call, or narrow the query, and only then draw a conclusion. If you already know the thread, do not search for it at all \u2014 read it directly with gog_gmail_thread_get, which returns the whole thread and cannot be truncated or mis-ranked.',
|
|
@@ -24997,7 +25361,7 @@ function registerGmailTools(server) {
|
|
|
24997
25361
|
return runOrDiagnose(args, { account });
|
|
24998
25362
|
});
|
|
24999
25363
|
server.registerTool("gog_gmail_send", {
|
|
25000
|
-
description: 'Send an email. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded.',
|
|
25364
|
+
description: 'Send an email. Two ways to attach a file: `attach` takes paths READ ON THE GOG SERVER, and `attachInline` takes the bytes themselves. Use attachInline unless you know the file exists on the same machine gog runs on \u2014 on the hosted connector and any remote deployment there is no shared filesystem, so no path you can name resolves there and `attach` will fail with "no such file or directory". When either is used, the JSON result echoes the attached filenames and byte sizes \u2014 check it to confirm the files were embedded. NOT the tool for answering a message: replyToMessageId only files this in the right thread \u2014 the subject, recipients and body are entirely yours, and the original is not quoted unless you set quote. Use gog_gmail_reply / gog_gmail_reply_all instead, which inherit all three.',
|
|
25001
25365
|
annotations: { destructiveHint: true },
|
|
25002
25366
|
inputSchema: {
|
|
25003
25367
|
to: external_exports.string().describe("Recipient(s), comma-separated"),
|
|
@@ -25005,23 +25369,43 @@ function registerGmailTools(server) {
|
|
|
25005
25369
|
body: external_exports.string().describe("Email body (plain text). Any size \u2014 a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body."),
|
|
25006
25370
|
cc: external_exports.string().optional().describe("CC recipients, comma-separated"),
|
|
25007
25371
|
bcc: external_exports.string().optional().describe("BCC recipients, comma-separated"),
|
|
25008
|
-
replyToMessageId: external_exports.string().optional().describe(
|
|
25009
|
-
threadId: external_exports.string().optional().describe("Thread ID to
|
|
25372
|
+
replyToMessageId: external_exports.string().optional().describe('Message ID to thread this message against \u2014 sets In-Reply-To/References only. It does NOT quote the original (pass quote for that), inherit its recipients, or prefix the subject with "Re:". For an actual reply use gog_gmail_reply.'),
|
|
25373
|
+
threadId: external_exports.string().optional().describe("Thread ID to thread this message within. Same caveat as replyToMessageId: threading only, no quote and no inherited subject or recipients."),
|
|
25374
|
+
quote: external_exports.boolean().optional().describe("Include the original message quoted below the body. Requires replyToMessageId or threadId. gog quotes by DEFAULT on gmail reply but never on gmail send, so without this a threaded send arrives with the original nowhere in it."),
|
|
25010
25375
|
attach: external_exports.array(external_exports.string()).optional().describe(`File paths to attach (repeatable), resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend these paths do not exist and the call fails with "no such file or directory" \u2014 use attachInline there. Each file is read on the server, base64-encoded with a MIME type inferred from its extension, and added as a multipart attachment.`),
|
|
25011
25376
|
attachInline: attachInlineParam,
|
|
25012
25377
|
account: accountParam
|
|
25013
25378
|
}
|
|
25014
|
-
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, attachInline, account }) => {
|
|
25379
|
+
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, quote, attach, attachInline, account }) => {
|
|
25015
25380
|
const args = ["gmail", "send", `--to=${to}`, `--subject=${subject}`, payloadArg("body", "body-file", body)];
|
|
25016
25381
|
if (cc) args.push(`--cc=${cc}`);
|
|
25017
25382
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
25018
25383
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
|
25019
25384
|
if (threadId) args.push(`--thread-id=${threadId}`);
|
|
25385
|
+
if (quote) args.push("--quote");
|
|
25020
25386
|
if (attach) for (const path of attach) args.push(`--attach=${path}`);
|
|
25021
25387
|
const inline = inlineAttachmentArgs("attach", attachInline, args);
|
|
25022
25388
|
args.push(...inline);
|
|
25023
25389
|
return runOrDiagnose(args, { account });
|
|
25024
25390
|
});
|
|
25391
|
+
server.registerTool("gog_gmail_reply", {
|
|
25392
|
+
description: 'Reply to a Gmail message (goes to the original sender only). USE THIS, not gog_gmail_send, whenever you are answering a message: it threads off the original AND inherits its "Re:" subject and quotes its body below yours, which gog_gmail_send does not \u2014 a send with replyToMessageId lands in the right thread but reads as a brand-new message, with the original nowhere in it. To answer every participant use gog_gmail_reply_all. The gogcli-mcp-gmail package adds two more routes with the same composition: gog_gmail_autoreply to reply across every message matching a query, and gog_gmail_drafts_reply to stage this exact reply as a draft instead of sending it.',
|
|
25393
|
+
annotations: { destructiveHint: true },
|
|
25394
|
+
inputSchema: replySchema
|
|
25395
|
+
}, async ({ messageId, account, ...flags }) => {
|
|
25396
|
+
const args = ["gmail", "reply", messageId];
|
|
25397
|
+
appendReplyFlags(args, flags);
|
|
25398
|
+
return runOrDiagnose(args, { account });
|
|
25399
|
+
});
|
|
25400
|
+
server.registerTool("gog_gmail_reply_all", {
|
|
25401
|
+
description: 'Reply to all participants of a Gmail message (the sender plus every To/Cc recipient). Same inherited "Re:" subject and quoted original as gog_gmail_reply. Use the remove flag to drop specific recipients from the reply-all. To stage it as a draft rather than send it, use gog_gmail_drafts_reply_all (gogcli-mcp-gmail only).',
|
|
25402
|
+
annotations: { destructiveHint: true },
|
|
25403
|
+
inputSchema: replySchema
|
|
25404
|
+
}, async ({ messageId, account, ...flags }) => {
|
|
25405
|
+
const args = ["gmail", "reply-all", messageId];
|
|
25406
|
+
appendReplyFlags(args, flags);
|
|
25407
|
+
return runOrDiagnose(args, { account });
|
|
25408
|
+
});
|
|
25025
25409
|
registerRunTool(server, { service: "gmail", examples: '"archive", "mark-read", "labels"' });
|
|
25026
25410
|
}
|
|
25027
25411
|
|
|
@@ -25349,11 +25733,13 @@ function registerTasksTools(server) {
|
|
|
25349
25733
|
}
|
|
25350
25734
|
|
|
25351
25735
|
// src/server.ts
|
|
25352
|
-
var VERSION = true ? "2.
|
|
25736
|
+
var VERSION = true ? "2.27.0" : "0.0.0";
|
|
25353
25737
|
var BASE_TOOL_REGISTRARS = [
|
|
25354
25738
|
registerApiTools,
|
|
25739
|
+
registerAppScriptTools,
|
|
25355
25740
|
registerAuthTools,
|
|
25356
25741
|
registerCalendarTools,
|
|
25742
|
+
registerChatTools,
|
|
25357
25743
|
registerClassroomTools,
|
|
25358
25744
|
registerContactsTools,
|
|
25359
25745
|
registerDocsTools,
|
|
@@ -25858,6 +26244,8 @@ export {
|
|
|
25858
26244
|
VERSION,
|
|
25859
26245
|
accountParam,
|
|
25860
26246
|
annotateTruncatedList,
|
|
26247
|
+
appendReplyFlags,
|
|
26248
|
+
assertNotBoth,
|
|
25861
26249
|
attachInlineParam,
|
|
25862
26250
|
authToolsFor,
|
|
25863
26251
|
diagnose,
|
|
@@ -25876,8 +26264,10 @@ export {
|
|
|
25876
26264
|
payloadArg,
|
|
25877
26265
|
pushPaginationFlags,
|
|
25878
26266
|
registerApiTools,
|
|
26267
|
+
registerAppScriptTools,
|
|
25879
26268
|
registerAuthTools,
|
|
25880
26269
|
registerCalendarTools,
|
|
26270
|
+
registerChatTools,
|
|
25881
26271
|
registerClassroomTools,
|
|
25882
26272
|
registerContactsTools,
|
|
25883
26273
|
registerDocsTools,
|
|
@@ -25887,6 +26277,7 @@ export {
|
|
|
25887
26277
|
registerSheetsTools,
|
|
25888
26278
|
registerSlidesTools,
|
|
25889
26279
|
registerTasksTools,
|
|
26280
|
+
replySchema,
|
|
25890
26281
|
resolvePageToken,
|
|
25891
26282
|
run,
|
|
25892
26283
|
runBinary,
|