gogcli-mcp-docs 2.0.8 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +319 -71
- package/manifest.json +17 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/docs-extra.ts +123 -5
- package/tests/tools/docs-extra.test.ts +128 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
|
|
10
|
-
"version": "2.0.
|
|
10
|
+
"version": "2.0.10"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli (Docs)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
|
|
18
|
-
"version": "2.0.
|
|
18
|
+
"version": "2.0.10",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -3105,6 +3105,9 @@ var require_utils = __commonJS({
|
|
|
3105
3105
|
"use strict";
|
|
3106
3106
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
3107
3107
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
3108
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
3109
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
3110
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
3108
3111
|
function stringArrayToHexStripped(input) {
|
|
3109
3112
|
let acc = "";
|
|
3110
3113
|
let code = 0;
|
|
@@ -3297,27 +3300,77 @@ var require_utils = __commonJS({
|
|
|
3297
3300
|
}
|
|
3298
3301
|
return output.join("");
|
|
3299
3302
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3303
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
3304
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
3305
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
3306
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
3307
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
3308
|
+
re.lastIndex = 0;
|
|
3309
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
3310
|
+
}
|
|
3311
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
3312
|
+
if (input.indexOf("%") === -1) {
|
|
3313
|
+
return input;
|
|
3310
3314
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3315
|
+
let output = "";
|
|
3316
|
+
for (let i = 0; i < input.length; i++) {
|
|
3317
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3318
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3319
|
+
if (isHexPair(hex3)) {
|
|
3320
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3321
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3322
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
3323
|
+
output += decoded;
|
|
3324
|
+
} else {
|
|
3325
|
+
output += "%" + normalizedHex;
|
|
3326
|
+
}
|
|
3327
|
+
i += 2;
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
output += input[i];
|
|
3313
3332
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3333
|
+
return output;
|
|
3334
|
+
}
|
|
3335
|
+
function normalizePathEncoding(input) {
|
|
3336
|
+
let output = "";
|
|
3337
|
+
for (let i = 0; i < input.length; i++) {
|
|
3338
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3339
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3340
|
+
if (isHexPair(hex3)) {
|
|
3341
|
+
const normalizedHex = hex3.toUpperCase();
|
|
3342
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
3343
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
3344
|
+
output += decoded;
|
|
3345
|
+
} else {
|
|
3346
|
+
output += "%" + normalizedHex;
|
|
3347
|
+
}
|
|
3348
|
+
i += 2;
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
if (isPathCharacter(input[i])) {
|
|
3353
|
+
output += input[i];
|
|
3354
|
+
} else {
|
|
3355
|
+
output += escape(input[i]);
|
|
3356
|
+
}
|
|
3316
3357
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3358
|
+
return output;
|
|
3359
|
+
}
|
|
3360
|
+
function escapePreservingEscapes(input) {
|
|
3361
|
+
let output = "";
|
|
3362
|
+
for (let i = 0; i < input.length; i++) {
|
|
3363
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
3364
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
3365
|
+
if (isHexPair(hex3)) {
|
|
3366
|
+
output += "%" + hex3.toUpperCase();
|
|
3367
|
+
i += 2;
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
output += escape(input[i]);
|
|
3319
3372
|
}
|
|
3320
|
-
return
|
|
3373
|
+
return output;
|
|
3321
3374
|
}
|
|
3322
3375
|
function recomposeAuthority(component) {
|
|
3323
3376
|
const uriTokens = [];
|
|
@@ -3332,7 +3385,7 @@ var require_utils = __commonJS({
|
|
|
3332
3385
|
if (ipV6res.isIPV6 === true) {
|
|
3333
3386
|
host = `[${ipV6res.escapedHost}]`;
|
|
3334
3387
|
} else {
|
|
3335
|
-
host =
|
|
3388
|
+
host = reescapeHostDelimiters(host, false);
|
|
3336
3389
|
}
|
|
3337
3390
|
}
|
|
3338
3391
|
uriTokens.push(host);
|
|
@@ -3346,7 +3399,10 @@ var require_utils = __commonJS({
|
|
|
3346
3399
|
module.exports = {
|
|
3347
3400
|
nonSimpleDomain,
|
|
3348
3401
|
recomposeAuthority,
|
|
3349
|
-
|
|
3402
|
+
reescapeHostDelimiters,
|
|
3403
|
+
normalizePercentEncoding,
|
|
3404
|
+
normalizePathEncoding,
|
|
3405
|
+
escapePreservingEscapes,
|
|
3350
3406
|
removeDotSegments,
|
|
3351
3407
|
isIPv4,
|
|
3352
3408
|
isUUID,
|
|
@@ -3570,12 +3626,12 @@ var require_schemes = __commonJS({
|
|
|
3570
3626
|
var require_fast_uri = __commonJS({
|
|
3571
3627
|
"../../node_modules/fast-uri/index.js"(exports, module) {
|
|
3572
3628
|
"use strict";
|
|
3573
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
3629
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
3574
3630
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
3575
3631
|
function normalize(uri, options) {
|
|
3576
3632
|
if (typeof uri === "string") {
|
|
3577
3633
|
uri = /** @type {T} */
|
|
3578
|
-
|
|
3634
|
+
normalizeString(uri, options);
|
|
3579
3635
|
} else if (typeof uri === "object") {
|
|
3580
3636
|
uri = /** @type {T} */
|
|
3581
3637
|
parse3(serialize(uri, options), options);
|
|
@@ -3642,19 +3698,9 @@ var require_fast_uri = __commonJS({
|
|
|
3642
3698
|
return target;
|
|
3643
3699
|
}
|
|
3644
3700
|
function equal(uriA, uriB, options) {
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
} else if (typeof uriA === "object") {
|
|
3649
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
3650
|
-
}
|
|
3651
|
-
if (typeof uriB === "string") {
|
|
3652
|
-
uriB = unescape(uriB);
|
|
3653
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
3654
|
-
} else if (typeof uriB === "object") {
|
|
3655
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
3656
|
-
}
|
|
3657
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
3701
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
3702
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
3703
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
3658
3704
|
}
|
|
3659
3705
|
function serialize(cmpts, opts) {
|
|
3660
3706
|
const component = {
|
|
@@ -3679,12 +3725,12 @@ var require_fast_uri = __commonJS({
|
|
|
3679
3725
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
3680
3726
|
if (component.path !== void 0) {
|
|
3681
3727
|
if (!options.skipEscape) {
|
|
3682
|
-
component.path =
|
|
3728
|
+
component.path = escapePreservingEscapes(component.path);
|
|
3683
3729
|
if (component.scheme !== void 0) {
|
|
3684
3730
|
component.path = component.path.split("%3A").join(":");
|
|
3685
3731
|
}
|
|
3686
3732
|
} else {
|
|
3687
|
-
component.path =
|
|
3733
|
+
component.path = normalizePercentEncoding(component.path);
|
|
3688
3734
|
}
|
|
3689
3735
|
}
|
|
3690
3736
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -3719,7 +3765,16 @@ var require_fast_uri = __commonJS({
|
|
|
3719
3765
|
return uriTokens.join("");
|
|
3720
3766
|
}
|
|
3721
3767
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3722
|
-
function
|
|
3768
|
+
function getParseError(parsed, matches) {
|
|
3769
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3770
|
+
return 'URI path must start with "/" when authority is present.';
|
|
3771
|
+
}
|
|
3772
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
3773
|
+
return "URI port is malformed.";
|
|
3774
|
+
}
|
|
3775
|
+
return void 0;
|
|
3776
|
+
}
|
|
3777
|
+
function parseWithStatus(uri, opts) {
|
|
3723
3778
|
const options = Object.assign({}, opts);
|
|
3724
3779
|
const parsed = {
|
|
3725
3780
|
scheme: void 0,
|
|
@@ -3730,6 +3785,7 @@ var require_fast_uri = __commonJS({
|
|
|
3730
3785
|
query: void 0,
|
|
3731
3786
|
fragment: void 0
|
|
3732
3787
|
};
|
|
3788
|
+
let malformedAuthorityOrPort = false;
|
|
3733
3789
|
let isIP = false;
|
|
3734
3790
|
if (options.reference === "suffix") {
|
|
3735
3791
|
if (options.scheme) {
|
|
@@ -3750,6 +3806,11 @@ var require_fast_uri = __commonJS({
|
|
|
3750
3806
|
if (isNaN(parsed.port)) {
|
|
3751
3807
|
parsed.port = matches[5];
|
|
3752
3808
|
}
|
|
3809
|
+
const parseError = getParseError(parsed, matches);
|
|
3810
|
+
if (parseError !== void 0) {
|
|
3811
|
+
parsed.error = parsed.error || parseError;
|
|
3812
|
+
malformedAuthorityOrPort = true;
|
|
3813
|
+
}
|
|
3753
3814
|
if (parsed.host) {
|
|
3754
3815
|
const ipv4result = isIPv4(parsed.host);
|
|
3755
3816
|
if (ipv4result === false) {
|
|
@@ -3788,14 +3849,18 @@ var require_fast_uri = __commonJS({
|
|
|
3788
3849
|
parsed.scheme = unescape(parsed.scheme);
|
|
3789
3850
|
}
|
|
3790
3851
|
if (parsed.host !== void 0) {
|
|
3791
|
-
parsed.host = unescape(parsed.host);
|
|
3852
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
3792
3853
|
}
|
|
3793
3854
|
}
|
|
3794
3855
|
if (parsed.path) {
|
|
3795
|
-
parsed.path =
|
|
3856
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
3796
3857
|
}
|
|
3797
3858
|
if (parsed.fragment) {
|
|
3798
|
-
|
|
3859
|
+
try {
|
|
3860
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
3861
|
+
} catch {
|
|
3862
|
+
parsed.error = parsed.error || "URI malformed";
|
|
3863
|
+
}
|
|
3799
3864
|
}
|
|
3800
3865
|
}
|
|
3801
3866
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -3804,7 +3869,29 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3869
|
} else {
|
|
3805
3870
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
3806
3871
|
}
|
|
3807
|
-
return parsed;
|
|
3872
|
+
return { parsed, malformedAuthorityOrPort };
|
|
3873
|
+
}
|
|
3874
|
+
function parse3(uri, opts) {
|
|
3875
|
+
return parseWithStatus(uri, opts).parsed;
|
|
3876
|
+
}
|
|
3877
|
+
function normalizeString(uri, opts) {
|
|
3878
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
3879
|
+
}
|
|
3880
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
3881
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
3882
|
+
return {
|
|
3883
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
3884
|
+
malformedAuthorityOrPort
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
function normalizeComparableURI(uri, opts) {
|
|
3888
|
+
if (typeof uri === "string") {
|
|
3889
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
3890
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
3891
|
+
}
|
|
3892
|
+
if (typeof uri === "object") {
|
|
3893
|
+
return serialize(uri, opts);
|
|
3894
|
+
}
|
|
3808
3895
|
}
|
|
3809
3896
|
var fastUri = {
|
|
3810
3897
|
SCHEMES,
|
|
@@ -9886,14 +9973,14 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
9886
9973
|
return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
|
|
9887
9974
|
};
|
|
9888
9975
|
doc.write(`const input = payload.value;`);
|
|
9889
|
-
const
|
|
9976
|
+
const ids2 = /* @__PURE__ */ Object.create(null);
|
|
9890
9977
|
let counter = 0;
|
|
9891
9978
|
for (const key of normalized.keys) {
|
|
9892
|
-
|
|
9979
|
+
ids2[key] = `key_${counter++}`;
|
|
9893
9980
|
}
|
|
9894
9981
|
doc.write(`const newResult = {};`);
|
|
9895
9982
|
for (const key of normalized.keys) {
|
|
9896
|
-
const id =
|
|
9983
|
+
const id = ids2[key];
|
|
9897
9984
|
const k = esc(key);
|
|
9898
9985
|
const schema = shape[key];
|
|
9899
9986
|
const isOptionalIn = schema?._zod?.optin === "optional";
|
|
@@ -30870,6 +30957,32 @@ function envOrUndefined(key) {
|
|
|
30870
30957
|
if (!value || value.startsWith("${")) return void 0;
|
|
30871
30958
|
return value;
|
|
30872
30959
|
}
|
|
30960
|
+
function sanitizedEnv() {
|
|
30961
|
+
const result = {};
|
|
30962
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
30963
|
+
if (key === "GOG_ACCESS_TOKEN") continue;
|
|
30964
|
+
if (key === "GOOGLE_APPLICATION_CREDENTIALS") continue;
|
|
30965
|
+
if (/(_TOKEN|_SECRET|_API_KEY|_PRIVATE_KEY)$/.test(key)) continue;
|
|
30966
|
+
result[key] = value;
|
|
30967
|
+
}
|
|
30968
|
+
return result;
|
|
30969
|
+
}
|
|
30970
|
+
var TOKEN_PATTERNS = [
|
|
30971
|
+
/Bearer\s+[A-Za-z0-9._\-+/=]+/gi,
|
|
30972
|
+
/ya29\.[A-Za-z0-9._\-]+/g,
|
|
30973
|
+
// OAuth2 access tokens
|
|
30974
|
+
/1\/\/[A-Za-z0-9._\-]+/g,
|
|
30975
|
+
// OAuth2 refresh tokens
|
|
30976
|
+
/AIza[A-Za-z0-9_\-]{35}/g
|
|
30977
|
+
// Google API keys
|
|
30978
|
+
];
|
|
30979
|
+
function redactSecrets(text) {
|
|
30980
|
+
let redacted = text;
|
|
30981
|
+
for (const re of TOKEN_PATTERNS) {
|
|
30982
|
+
redacted = redacted.replace(re, "[REDACTED]");
|
|
30983
|
+
}
|
|
30984
|
+
return redacted;
|
|
30985
|
+
}
|
|
30873
30986
|
function augmentedPath() {
|
|
30874
30987
|
const home = process.env.HOME;
|
|
30875
30988
|
const candidates = [
|
|
@@ -30912,8 +31025,7 @@ async function run(args, options = {}) {
|
|
|
30912
31025
|
fullArgs.push(...args);
|
|
30913
31026
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
30914
31027
|
return new Promise((resolve, reject) => {
|
|
30915
|
-
const
|
|
30916
|
-
const childEnv = { ...cleanEnv, PATH: augmentedPath() };
|
|
31028
|
+
const childEnv = { ...sanitizedEnv(), PATH: augmentedPath() };
|
|
30917
31029
|
const child = spawner(envOrUndefined("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
|
|
30918
31030
|
const stdoutChunks = [];
|
|
30919
31031
|
const stderrChunks = [];
|
|
@@ -30942,7 +31054,7 @@ async function run(args, options = {}) {
|
|
|
30942
31054
|
resolve(stdout);
|
|
30943
31055
|
}
|
|
30944
31056
|
} else {
|
|
30945
|
-
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
31057
|
+
reject(new Error(redactSecrets(stderr || `gog exited with code ${code}`)));
|
|
30946
31058
|
}
|
|
30947
31059
|
});
|
|
30948
31060
|
child.on("error", (err) => {
|
|
@@ -30964,6 +31076,55 @@ async function run(args, options = {}) {
|
|
|
30964
31076
|
var accountParam = external_exports.string().optional().describe(
|
|
30965
31077
|
"Google account email to use (overrides GOG_ACCOUNT env var)"
|
|
30966
31078
|
);
|
|
31079
|
+
var ids = {
|
|
31080
|
+
course: external_exports.string().describe("Course ID"),
|
|
31081
|
+
coursework: external_exports.string().describe("Coursework ID"),
|
|
31082
|
+
submission: external_exports.string().describe("Submission ID"),
|
|
31083
|
+
announcement: external_exports.string().describe("Announcement ID"),
|
|
31084
|
+
topic: external_exports.string().describe("Topic ID"),
|
|
31085
|
+
invitation: external_exports.string().describe("Invitation ID"),
|
|
31086
|
+
spreadsheet: external_exports.string().describe("Spreadsheet ID (from the URL)"),
|
|
31087
|
+
doc: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31088
|
+
presentation: external_exports.string().describe("Presentation ID"),
|
|
31089
|
+
slide: external_exports.string().describe("Slide ID"),
|
|
31090
|
+
file: external_exports.string().describe("File ID"),
|
|
31091
|
+
message: external_exports.string().describe("Message ID"),
|
|
31092
|
+
thread: external_exports.string().describe("Thread ID"),
|
|
31093
|
+
draft: external_exports.string().describe("Draft ID"),
|
|
31094
|
+
label: external_exports.string().describe("Label ID or name"),
|
|
31095
|
+
attachment: external_exports.string().describe("Attachment ID"),
|
|
31096
|
+
comment: external_exports.string().describe("Comment ID"),
|
|
31097
|
+
meetingCode: external_exports.string().describe("Meeting code (e.g. abc-defg-hij)"),
|
|
31098
|
+
permission: external_exports.string().describe("Permission ID"),
|
|
31099
|
+
user: external_exports.string().describe("User ID"),
|
|
31100
|
+
// People API uses fully-qualified resource names ("people/c123") not bare IDs.
|
|
31101
|
+
person: external_exports.string().describe("Person resource name (people/...) or email")
|
|
31102
|
+
};
|
|
31103
|
+
var paginationParams = {
|
|
31104
|
+
max: external_exports.number().int().optional().describe("Max results"),
|
|
31105
|
+
page: external_exports.string().optional().describe("Page token"),
|
|
31106
|
+
all: external_exports.boolean().optional().describe("Fetch all pages")
|
|
31107
|
+
};
|
|
31108
|
+
function registerRunTool(server2, options) {
|
|
31109
|
+
const { service, examples, omitAccount = false, note } = options;
|
|
31110
|
+
const baseDescription = `Run any gog ${service} subcommand not covered by the other tools. Run \`gog ${service} --help\` for the full list of subcommands, or \`gog ${service} <subcommand> --help\` for flags on a specific subcommand.`;
|
|
31111
|
+
const description = note ? `${baseDescription} ${note}` : baseDescription;
|
|
31112
|
+
const inputSchema = {
|
|
31113
|
+
subcommand: external_exports.string().describe(`The gog ${service} subcommand to run, e.g. ${examples}`),
|
|
31114
|
+
args: external_exports.array(external_exports.string()).describe("Additional positional args and flags")
|
|
31115
|
+
};
|
|
31116
|
+
if (!omitAccount) {
|
|
31117
|
+
inputSchema.account = accountParam;
|
|
31118
|
+
}
|
|
31119
|
+
server2.registerTool(`gog_${service}_run`, {
|
|
31120
|
+
description,
|
|
31121
|
+
annotations: { destructiveHint: true },
|
|
31122
|
+
inputSchema
|
|
31123
|
+
}, async (rawArgs) => {
|
|
31124
|
+
const { subcommand, args, account } = rawArgs;
|
|
31125
|
+
return runOrDiagnose([service, subcommand, ...args], { account });
|
|
31126
|
+
});
|
|
31127
|
+
}
|
|
30967
31128
|
function toText(output) {
|
|
30968
31129
|
return { content: [{ type: "text", text: output }] };
|
|
30969
31130
|
}
|
|
@@ -31049,15 +31210,11 @@ function registerAuthTools(server2) {
|
|
|
31049
31210
|
return toError(err);
|
|
31050
31211
|
}
|
|
31051
31212
|
});
|
|
31052
|
-
server2
|
|
31053
|
-
|
|
31054
|
-
|
|
31055
|
-
|
|
31056
|
-
|
|
31057
|
-
args: external_exports.array(external_exports.string()).describe("Additional positional args and flags")
|
|
31058
|
-
}
|
|
31059
|
-
}, async ({ subcommand, args }) => {
|
|
31060
|
-
return runOrDiagnose(["auth", subcommand, ...args], {});
|
|
31213
|
+
registerRunTool(server2, {
|
|
31214
|
+
service: "auth",
|
|
31215
|
+
examples: '"remove", "alias", "tokens"',
|
|
31216
|
+
omitAccount: true,
|
|
31217
|
+
note: "For browser-based authorization, use gog_auth_add instead."
|
|
31061
31218
|
});
|
|
31062
31219
|
}
|
|
31063
31220
|
|
|
@@ -31128,21 +31285,14 @@ function registerDocsTools(server2) {
|
|
|
31128
31285
|
}, async ({ docId, account }) => {
|
|
31129
31286
|
return runOrDiagnose(["docs", "structure", docId], { account });
|
|
31130
31287
|
});
|
|
31131
|
-
server2
|
|
31132
|
-
description: "Run any gog docs subcommand not covered by the other tools. Run `gog docs --help` for the full list of subcommands, or `gog docs <subcommand> --help` for flags on a specific subcommand.",
|
|
31133
|
-
annotations: { destructiveHint: true },
|
|
31134
|
-
inputSchema: {
|
|
31135
|
-
subcommand: external_exports.string().describe('The gog docs subcommand to run, e.g. "copy", "clear", "insert", "sed", "export"'),
|
|
31136
|
-
args: external_exports.array(external_exports.string()).describe("Additional positional args and flags"),
|
|
31137
|
-
account: accountParam
|
|
31138
|
-
}
|
|
31139
|
-
}, async ({ subcommand, args, account }) => {
|
|
31140
|
-
return runOrDiagnose(["docs", subcommand, ...args], { account });
|
|
31141
|
-
});
|
|
31288
|
+
registerRunTool(server2, { service: "docs", examples: '"copy", "clear", "insert", "sed", "export"' });
|
|
31142
31289
|
}
|
|
31143
31290
|
|
|
31291
|
+
// ../gogcli-mcp/src/tools/sheets.ts
|
|
31292
|
+
var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
|
|
31293
|
+
|
|
31144
31294
|
// ../gogcli-mcp/src/server.ts
|
|
31145
|
-
var VERSION = true ? "2.0.
|
|
31295
|
+
var VERSION = true ? "2.0.10" : "0.0.0";
|
|
31146
31296
|
function createServer(options) {
|
|
31147
31297
|
return new McpServer({
|
|
31148
31298
|
name: options?.name ?? "gogcli",
|
|
@@ -31166,7 +31316,7 @@ function registerExtraDocsTools(server2) {
|
|
|
31166
31316
|
return runOrDiagnose(args, { account });
|
|
31167
31317
|
});
|
|
31168
31318
|
server2.registerTool("gog_docs_delete", {
|
|
31169
|
-
description: "Delete content within a Google Doc by character index range.",
|
|
31319
|
+
description: "Delete content within a Google Doc by character index range. To remove the entire document (move to Drive trash), use gog_docs_trash.",
|
|
31170
31320
|
annotations: { destructiveHint: true },
|
|
31171
31321
|
inputSchema: {
|
|
31172
31322
|
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
@@ -31180,6 +31330,16 @@ function registerExtraDocsTools(server2) {
|
|
|
31180
31330
|
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
31181
31331
|
return runOrDiagnose(args, { account });
|
|
31182
31332
|
});
|
|
31333
|
+
server2.registerTool("gog_docs_trash", {
|
|
31334
|
+
description: "Move an entire Google Doc to Drive trash. Convenience wrapper around `gog drive delete` so docs-only users can clean up without installing gogcli-mcp-drive. The doc remains recoverable from Drive trash for ~30 days.",
|
|
31335
|
+
annotations: { destructiveHint: true },
|
|
31336
|
+
inputSchema: {
|
|
31337
|
+
docId: external_exports.string().describe("Doc ID to move to trash"),
|
|
31338
|
+
account: accountParam
|
|
31339
|
+
}
|
|
31340
|
+
}, async ({ docId, account }) => {
|
|
31341
|
+
return runOrDiagnose(["drive", "delete", docId], { account });
|
|
31342
|
+
});
|
|
31183
31343
|
server2.registerTool("gog_docs_edit", {
|
|
31184
31344
|
description: "Edit a Google Doc by finding and replacing text (stream-edit style).",
|
|
31185
31345
|
annotations: { destructiveHint: true },
|
|
@@ -31195,6 +31355,75 @@ function registerExtraDocsTools(server2) {
|
|
|
31195
31355
|
if (matchCase) args.push("--match-case");
|
|
31196
31356
|
return runOrDiagnose(args, { account });
|
|
31197
31357
|
});
|
|
31358
|
+
server2.registerTool("gog_docs_read", {
|
|
31359
|
+
description: 'Read the content of a Google Doc. Default: plain text body. Use format="json" for the raw Google Docs API response (lossless, includes character indices needed for index-based gog_docs_insert / gog_docs_delete calls). For markdown output, use gog_docs_export with format="md" \u2014 it writes to a file. Use gog_docs_structure to see paragraph-by-paragraph layout with indices.',
|
|
31360
|
+
annotations: { readOnlyHint: true },
|
|
31361
|
+
inputSchema: {
|
|
31362
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31363
|
+
format: external_exports.enum(["text", "json"]).optional().describe("Output format (default: text)"),
|
|
31364
|
+
tab: external_exports.string().optional().describe("Target tab title or ID (text mode only)"),
|
|
31365
|
+
allTabs: external_exports.boolean().optional().describe("Show all tabs with headers (text mode only)"),
|
|
31366
|
+
maxBytes: external_exports.number().optional().describe("Max bytes to read in text mode (0 = unlimited; default 2000000)"),
|
|
31367
|
+
account: accountParam
|
|
31368
|
+
}
|
|
31369
|
+
}, async ({ docId, format, tab, allTabs, maxBytes, account }) => {
|
|
31370
|
+
if (format === "json") {
|
|
31371
|
+
return runOrDiagnose(["docs", "raw", docId, "--pretty"], { account });
|
|
31372
|
+
}
|
|
31373
|
+
const args = ["docs", "cat", docId];
|
|
31374
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
31375
|
+
if (allTabs) args.push("--all-tabs");
|
|
31376
|
+
if (maxBytes !== void 0) args.push(`--max-bytes=${maxBytes}`);
|
|
31377
|
+
return runOrDiagnose(args, { account });
|
|
31378
|
+
});
|
|
31379
|
+
server2.registerTool("gog_docs_format", {
|
|
31380
|
+
description: "Apply text or paragraph formatting to a Google Doc. Use `match` to format a specific text occurrence, `matchAll` to format every occurrence, or omit both to format the whole doc. Boolean flags (bold/italic/etc.) set the attribute; negated flags (noBold/noItalic/etc.) clear it.",
|
|
31381
|
+
annotations: { destructiveHint: true },
|
|
31382
|
+
inputSchema: {
|
|
31383
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31384
|
+
match: external_exports.string().optional().describe("Format only the first text match"),
|
|
31385
|
+
matchAll: external_exports.boolean().optional().describe("Format all matches instead of only the first"),
|
|
31386
|
+
matchCase: external_exports.boolean().optional().describe("Case-sensitive matching"),
|
|
31387
|
+
tab: external_exports.string().optional().describe("Target tab title or ID"),
|
|
31388
|
+
fontFamily: external_exports.string().optional().describe("Font family (e.g. Arial, Georgia)"),
|
|
31389
|
+
fontSize: external_exports.number().optional().describe("Font size in points"),
|
|
31390
|
+
textColor: external_exports.string().optional().describe("Text color as #RRGGBB or #RGB"),
|
|
31391
|
+
bgColor: external_exports.string().optional().describe("Text background color as #RRGGBB or #RGB"),
|
|
31392
|
+
bold: external_exports.boolean().optional().describe("Set bold"),
|
|
31393
|
+
noBold: external_exports.boolean().optional().describe("Clear bold"),
|
|
31394
|
+
italic: external_exports.boolean().optional().describe("Set italic"),
|
|
31395
|
+
noItalic: external_exports.boolean().optional().describe("Clear italic"),
|
|
31396
|
+
underline: external_exports.boolean().optional().describe("Set underline"),
|
|
31397
|
+
noUnderline: external_exports.boolean().optional().describe("Clear underline"),
|
|
31398
|
+
strikethrough: external_exports.boolean().optional().describe("Set strikethrough"),
|
|
31399
|
+
noStrikethrough: external_exports.boolean().optional().describe("Clear strikethrough"),
|
|
31400
|
+
alignment: external_exports.enum(["left", "center", "right", "justify", "start", "end", "justified"]).optional().describe("Paragraph alignment"),
|
|
31401
|
+
lineSpacing: external_exports.number().optional().describe("Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)"),
|
|
31402
|
+
account: accountParam
|
|
31403
|
+
}
|
|
31404
|
+
}, async (args) => {
|
|
31405
|
+
const a = args;
|
|
31406
|
+
const argv = ["docs", "format", a.docId];
|
|
31407
|
+
if (a.match) argv.push(`--match=${a.match}`);
|
|
31408
|
+
if (a.matchAll) argv.push("--match-all");
|
|
31409
|
+
if (a.matchCase) argv.push("--match-case");
|
|
31410
|
+
if (a.tab) argv.push(`--tab=${a.tab}`);
|
|
31411
|
+
if (a.fontFamily) argv.push(`--font-family=${a.fontFamily}`);
|
|
31412
|
+
if (a.fontSize !== void 0) argv.push(`--font-size=${a.fontSize}`);
|
|
31413
|
+
if (a.textColor) argv.push(`--text-color=${a.textColor}`);
|
|
31414
|
+
if (a.bgColor) argv.push(`--bg-color=${a.bgColor}`);
|
|
31415
|
+
if (a.bold) argv.push("--bold");
|
|
31416
|
+
if (a.noBold) argv.push("--no-bold");
|
|
31417
|
+
if (a.italic) argv.push("--italic");
|
|
31418
|
+
if (a.noItalic) argv.push("--no-italic");
|
|
31419
|
+
if (a.underline) argv.push("--underline");
|
|
31420
|
+
if (a.noUnderline) argv.push("--no-underline");
|
|
31421
|
+
if (a.strikethrough) argv.push("--strikethrough");
|
|
31422
|
+
if (a.noStrikethrough) argv.push("--no-strikethrough");
|
|
31423
|
+
if (a.alignment) argv.push(`--alignment=${a.alignment}`);
|
|
31424
|
+
if (a.lineSpacing !== void 0) argv.push(`--line-spacing=${a.lineSpacing}`);
|
|
31425
|
+
return runOrDiagnose(argv, { account: a.account });
|
|
31426
|
+
});
|
|
31198
31427
|
server2.registerTool("gog_docs_export", {
|
|
31199
31428
|
description: "Export a Google Doc as PDF, plain text, HTML, DOCX, or other format.",
|
|
31200
31429
|
annotations: { readOnlyHint: true },
|
|
@@ -31211,12 +31440,12 @@ function registerExtraDocsTools(server2) {
|
|
|
31211
31440
|
return runOrDiagnose(args, { account });
|
|
31212
31441
|
});
|
|
31213
31442
|
server2.registerTool("gog_docs_insert", {
|
|
31214
|
-
description: "Insert text at a specific
|
|
31443
|
+
description: "Insert text at a specific character index in a Google Doc. When `index` is omitted, gog defaults to 1 (the very beginning), NOT the end \u2014 sequential inserts without an explicit index produce reversed output. To append at the end of the doc, use gog_docs_append (which uses `gog docs write --append` and is the right tool for iterative document construction). To find a valid index for mid-document inserts, call gog_docs_structure or gog_docs_read first.",
|
|
31215
31444
|
annotations: { destructiveHint: true },
|
|
31216
31445
|
inputSchema: {
|
|
31217
31446
|
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31218
31447
|
content: external_exports.string().optional().describe("Text content to insert"),
|
|
31219
|
-
index: external_exports.number().optional().describe("Character index to insert at (default:
|
|
31448
|
+
index: external_exports.number().optional().describe("Character index to insert at (1-based; default: 1 = start of doc). Prefer gog_docs_append when you want to add at the end."),
|
|
31220
31449
|
file: external_exports.string().optional().describe("Path to a file whose content to insert"),
|
|
31221
31450
|
tabId: external_exports.string().optional().describe("Tab ID to insert into (for multi-tab docs)"),
|
|
31222
31451
|
account: accountParam
|
|
@@ -31229,6 +31458,25 @@ function registerExtraDocsTools(server2) {
|
|
|
31229
31458
|
if (tabId) args.push(`--tab-id=${tabId}`);
|
|
31230
31459
|
return runOrDiagnose(args, { account });
|
|
31231
31460
|
});
|
|
31461
|
+
server2.registerTool("gog_docs_append", {
|
|
31462
|
+
description: "Append text to the end of a Google Doc. This is the right tool for iterative document construction \u2014 multiple sequential calls produce content in the order they were called. Use gog_docs_insert only when you need to insert at a specific character position.",
|
|
31463
|
+
annotations: { destructiveHint: true },
|
|
31464
|
+
inputSchema: {
|
|
31465
|
+
docId: external_exports.string().describe("Doc ID (from the URL)"),
|
|
31466
|
+
text: external_exports.string().optional().describe("Text content to append"),
|
|
31467
|
+
file: external_exports.string().optional().describe('Path to a text file to append (use "-" for stdin)'),
|
|
31468
|
+
markdown: external_exports.boolean().optional().describe("Convert markdown to Google Docs formatting (headings, bold, lists, etc.)"),
|
|
31469
|
+
tab: external_exports.string().optional().describe("Target tab title or ID (for multi-tab docs)"),
|
|
31470
|
+
account: accountParam
|
|
31471
|
+
}
|
|
31472
|
+
}, async ({ docId, text, file: file2, markdown, tab, account }) => {
|
|
31473
|
+
const args = ["docs", "write", docId, "--append"];
|
|
31474
|
+
if (text) args.push(`--text=${text}`);
|
|
31475
|
+
if (file2) args.push(`--file=${file2}`);
|
|
31476
|
+
if (markdown) args.push("--markdown");
|
|
31477
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
31478
|
+
return runOrDiagnose(args, { account });
|
|
31479
|
+
});
|
|
31232
31480
|
server2.registerTool("gog_docs_list_tabs", {
|
|
31233
31481
|
description: "List all tabs in a Google Doc.",
|
|
31234
31482
|
annotations: { readOnlyHint: true },
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-docs",
|
|
5
5
|
"display_name": "gogcli (Docs)",
|
|
6
|
-
"version": "2.0.
|
|
6
|
+
"version": "2.0.10",
|
|
7
7
|
"description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -158,6 +158,22 @@
|
|
|
158
158
|
{
|
|
159
159
|
"name": "gog_docs_comments_delete",
|
|
160
160
|
"description": "Delete a comment from a Google Doc. This action is permanent."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "gog_docs_append",
|
|
164
|
+
"description": "Append text to the end of a Google Doc (the right tool for iterative construction; sequential calls produce in-order content)"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "gog_docs_read",
|
|
168
|
+
"description": "Read a Google Doc as plain text or as raw Docs API JSON"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "gog_docs_format",
|
|
172
|
+
"description": "Apply text/paragraph formatting (bold, italic, font size, color, alignment, line spacing) — supports match, matchAll, matchCase"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"name": "gog_docs_trash",
|
|
176
|
+
"description": "Move an entire Google Doc to Drive trash (convenience wrapper around gog drive delete)"
|
|
161
177
|
}
|
|
162
178
|
],
|
|
163
179
|
"compatibility": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-docs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-docs",
|
|
5
5
|
"description": "Extended Google Docs MCP server via gogcli — all base tools plus full Docs support",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp-docs"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.0.
|
|
10
|
+
"version": "2.0.10",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-docs",
|
|
15
|
-
"version": "2.0.
|
|
15
|
+
"version": "2.0.10",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/tools/docs-extra.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
server.registerTool('gog_docs_delete', {
|
|
21
|
-
description: 'Delete content within a Google Doc by character index range.',
|
|
21
|
+
description: 'Delete content within a Google Doc by character index range. To remove the entire document (move to Drive trash), use gog_docs_trash.',
|
|
22
22
|
annotations: { destructiveHint: true },
|
|
23
23
|
inputSchema: {
|
|
24
24
|
docId: z.string().describe('Doc ID (from the URL)'),
|
|
@@ -33,6 +33,17 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
33
33
|
return runOrDiagnose(args, { account });
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
server.registerTool('gog_docs_trash', {
|
|
37
|
+
description: 'Move an entire Google Doc to Drive trash. Convenience wrapper around `gog drive delete` so docs-only users can clean up without installing gogcli-mcp-drive. The doc remains recoverable from Drive trash for ~30 days.',
|
|
38
|
+
annotations: { destructiveHint: true },
|
|
39
|
+
inputSchema: {
|
|
40
|
+
docId: z.string().describe('Doc ID to move to trash'),
|
|
41
|
+
account: accountParam,
|
|
42
|
+
},
|
|
43
|
+
}, async ({ docId, account }) => {
|
|
44
|
+
return runOrDiagnose(['drive', 'delete', docId], { account });
|
|
45
|
+
});
|
|
46
|
+
|
|
36
47
|
server.registerTool('gog_docs_edit', {
|
|
37
48
|
description: 'Edit a Google Doc by finding and replacing text (stream-edit style).',
|
|
38
49
|
annotations: { destructiveHint: true },
|
|
@@ -49,6 +60,94 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
49
60
|
return runOrDiagnose(args, { account });
|
|
50
61
|
});
|
|
51
62
|
|
|
63
|
+
server.registerTool('gog_docs_read', {
|
|
64
|
+
description: 'Read the content of a Google Doc. Default: plain text body. Use format="json" for the raw Google Docs API response (lossless, includes character indices needed for index-based gog_docs_insert / gog_docs_delete calls). For markdown output, use gog_docs_export with format="md" — it writes to a file. Use gog_docs_structure to see paragraph-by-paragraph layout with indices.',
|
|
65
|
+
annotations: { readOnlyHint: true },
|
|
66
|
+
inputSchema: {
|
|
67
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
68
|
+
format: z.enum(['text', 'json']).optional().describe('Output format (default: text)'),
|
|
69
|
+
tab: z.string().optional().describe('Target tab title or ID (text mode only)'),
|
|
70
|
+
allTabs: z.boolean().optional().describe('Show all tabs with headers (text mode only)'),
|
|
71
|
+
maxBytes: z.number().optional().describe('Max bytes to read in text mode (0 = unlimited; default 2000000)'),
|
|
72
|
+
account: accountParam,
|
|
73
|
+
},
|
|
74
|
+
}, async ({ docId, format, tab, allTabs, maxBytes, account }) => {
|
|
75
|
+
if (format === 'json') {
|
|
76
|
+
return runOrDiagnose(['docs', 'raw', docId, '--pretty'], { account });
|
|
77
|
+
}
|
|
78
|
+
const args = ['docs', 'cat', docId];
|
|
79
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
80
|
+
if (allTabs) args.push('--all-tabs');
|
|
81
|
+
if (maxBytes !== undefined) args.push(`--max-bytes=${maxBytes}`);
|
|
82
|
+
return runOrDiagnose(args, { account });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
server.registerTool('gog_docs_format', {
|
|
86
|
+
description: 'Apply text or paragraph formatting to a Google Doc. Use `match` to format a specific text occurrence, `matchAll` to format every occurrence, or omit both to format the whole doc. Boolean flags (bold/italic/etc.) set the attribute; negated flags (noBold/noItalic/etc.) clear it.',
|
|
87
|
+
annotations: { destructiveHint: true },
|
|
88
|
+
inputSchema: {
|
|
89
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
90
|
+
match: z.string().optional().describe('Format only the first text match'),
|
|
91
|
+
matchAll: z.boolean().optional().describe('Format all matches instead of only the first'),
|
|
92
|
+
matchCase: z.boolean().optional().describe('Case-sensitive matching'),
|
|
93
|
+
tab: z.string().optional().describe('Target tab title or ID'),
|
|
94
|
+
fontFamily: z.string().optional().describe('Font family (e.g. Arial, Georgia)'),
|
|
95
|
+
fontSize: z.number().optional().describe('Font size in points'),
|
|
96
|
+
textColor: z.string().optional().describe('Text color as #RRGGBB or #RGB'),
|
|
97
|
+
bgColor: z.string().optional().describe('Text background color as #RRGGBB or #RGB'),
|
|
98
|
+
bold: z.boolean().optional().describe('Set bold'),
|
|
99
|
+
noBold: z.boolean().optional().describe('Clear bold'),
|
|
100
|
+
italic: z.boolean().optional().describe('Set italic'),
|
|
101
|
+
noItalic: z.boolean().optional().describe('Clear italic'),
|
|
102
|
+
underline: z.boolean().optional().describe('Set underline'),
|
|
103
|
+
noUnderline: z.boolean().optional().describe('Clear underline'),
|
|
104
|
+
strikethrough: z.boolean().optional().describe('Set strikethrough'),
|
|
105
|
+
noStrikethrough: z.boolean().optional().describe('Clear strikethrough'),
|
|
106
|
+
alignment: z.enum(['left', 'center', 'right', 'justify', 'start', 'end', 'justified']).optional().describe('Paragraph alignment'),
|
|
107
|
+
lineSpacing: z.number().optional().describe('Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)'),
|
|
108
|
+
account: accountParam,
|
|
109
|
+
},
|
|
110
|
+
}, async (args) => {
|
|
111
|
+
const a = args as {
|
|
112
|
+
docId: string;
|
|
113
|
+
match?: string;
|
|
114
|
+
matchAll?: boolean;
|
|
115
|
+
matchCase?: boolean;
|
|
116
|
+
tab?: string;
|
|
117
|
+
fontFamily?: string;
|
|
118
|
+
fontSize?: number;
|
|
119
|
+
textColor?: string;
|
|
120
|
+
bgColor?: string;
|
|
121
|
+
bold?: boolean; noBold?: boolean;
|
|
122
|
+
italic?: boolean; noItalic?: boolean;
|
|
123
|
+
underline?: boolean; noUnderline?: boolean;
|
|
124
|
+
strikethrough?: boolean; noStrikethrough?: boolean;
|
|
125
|
+
alignment?: string;
|
|
126
|
+
lineSpacing?: number;
|
|
127
|
+
account?: string;
|
|
128
|
+
};
|
|
129
|
+
const argv = ['docs', 'format', a.docId];
|
|
130
|
+
if (a.match) argv.push(`--match=${a.match}`);
|
|
131
|
+
if (a.matchAll) argv.push('--match-all');
|
|
132
|
+
if (a.matchCase) argv.push('--match-case');
|
|
133
|
+
if (a.tab) argv.push(`--tab=${a.tab}`);
|
|
134
|
+
if (a.fontFamily) argv.push(`--font-family=${a.fontFamily}`);
|
|
135
|
+
if (a.fontSize !== undefined) argv.push(`--font-size=${a.fontSize}`);
|
|
136
|
+
if (a.textColor) argv.push(`--text-color=${a.textColor}`);
|
|
137
|
+
if (a.bgColor) argv.push(`--bg-color=${a.bgColor}`);
|
|
138
|
+
if (a.bold) argv.push('--bold');
|
|
139
|
+
if (a.noBold) argv.push('--no-bold');
|
|
140
|
+
if (a.italic) argv.push('--italic');
|
|
141
|
+
if (a.noItalic) argv.push('--no-italic');
|
|
142
|
+
if (a.underline) argv.push('--underline');
|
|
143
|
+
if (a.noUnderline) argv.push('--no-underline');
|
|
144
|
+
if (a.strikethrough) argv.push('--strikethrough');
|
|
145
|
+
if (a.noStrikethrough) argv.push('--no-strikethrough');
|
|
146
|
+
if (a.alignment) argv.push(`--alignment=${a.alignment}`);
|
|
147
|
+
if (a.lineSpacing !== undefined) argv.push(`--line-spacing=${a.lineSpacing}`);
|
|
148
|
+
return runOrDiagnose(argv, { account: a.account });
|
|
149
|
+
});
|
|
150
|
+
|
|
52
151
|
server.registerTool('gog_docs_export', {
|
|
53
152
|
description: 'Export a Google Doc as PDF, plain text, HTML, DOCX, or other format.',
|
|
54
153
|
annotations: { readOnlyHint: true },
|
|
@@ -66,12 +165,12 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
66
165
|
});
|
|
67
166
|
|
|
68
167
|
server.registerTool('gog_docs_insert', {
|
|
69
|
-
description: 'Insert text at a specific
|
|
168
|
+
description: 'Insert text at a specific character index in a Google Doc. When `index` is omitted, gog defaults to 1 (the very beginning), NOT the end — sequential inserts without an explicit index produce reversed output. To append at the end of the doc, use gog_docs_append (which uses `gog docs write --append` and is the right tool for iterative document construction). To find a valid index for mid-document inserts, call gog_docs_structure or gog_docs_read first.',
|
|
70
169
|
annotations: { destructiveHint: true },
|
|
71
170
|
inputSchema: {
|
|
72
171
|
docId: z.string().describe('Doc ID (from the URL)'),
|
|
73
172
|
content: z.string().optional().describe('Text content to insert'),
|
|
74
|
-
index: z.number().optional().describe('Character index to insert at (default:
|
|
173
|
+
index: z.number().optional().describe('Character index to insert at (1-based; default: 1 = start of doc). Prefer gog_docs_append when you want to add at the end.'),
|
|
75
174
|
file: z.string().optional().describe('Path to a file whose content to insert'),
|
|
76
175
|
tabId: z.string().optional().describe('Tab ID to insert into (for multi-tab docs)'),
|
|
77
176
|
account: accountParam,
|
|
@@ -85,6 +184,26 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
85
184
|
return runOrDiagnose(args, { account });
|
|
86
185
|
});
|
|
87
186
|
|
|
187
|
+
server.registerTool('gog_docs_append', {
|
|
188
|
+
description: 'Append text to the end of a Google Doc. This is the right tool for iterative document construction — multiple sequential calls produce content in the order they were called. Use gog_docs_insert only when you need to insert at a specific character position.',
|
|
189
|
+
annotations: { destructiveHint: true },
|
|
190
|
+
inputSchema: {
|
|
191
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
192
|
+
text: z.string().optional().describe('Text content to append'),
|
|
193
|
+
file: z.string().optional().describe('Path to a text file to append (use "-" for stdin)'),
|
|
194
|
+
markdown: z.boolean().optional().describe('Convert markdown to Google Docs formatting (headings, bold, lists, etc.)'),
|
|
195
|
+
tab: z.string().optional().describe('Target tab title or ID (for multi-tab docs)'),
|
|
196
|
+
account: accountParam,
|
|
197
|
+
},
|
|
198
|
+
}, async ({ docId, text, file, markdown, tab, account }) => {
|
|
199
|
+
const args = ['docs', 'write', docId, '--append'];
|
|
200
|
+
if (text) args.push(`--text=${text}`);
|
|
201
|
+
if (file) args.push(`--file=${file}`);
|
|
202
|
+
if (markdown) args.push('--markdown');
|
|
203
|
+
if (tab) args.push(`--tab=${tab}`);
|
|
204
|
+
return runOrDiagnose(args, { account });
|
|
205
|
+
});
|
|
206
|
+
|
|
88
207
|
server.registerTool('gog_docs_list_tabs', {
|
|
89
208
|
description: 'List all tabs in a Google Doc.',
|
|
90
209
|
annotations: { readOnlyHint: true },
|
|
@@ -142,8 +261,7 @@ export function registerExtraDocsTools(server: McpServer): void {
|
|
|
142
261
|
return runOrDiagnose(args, { account });
|
|
143
262
|
});
|
|
144
263
|
|
|
145
|
-
//
|
|
146
|
-
|
|
264
|
+
// Comment-thread tools
|
|
147
265
|
server.registerTool('gog_docs_comments_list', {
|
|
148
266
|
description:
|
|
149
267
|
'List comments on a Google Doc. Returns open comments by default; set includeResolved=true to include resolved comments.',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { registerExtraDocsTools } from '../../src/tools/docs-extra.js';
|
|
3
3
|
import * as lib from '../../../gogcli-mcp/src/lib.js';
|
|
4
|
-
import {
|
|
4
|
+
import { setupHandlers as setupHandlersBase, toText } from '../../../gogcli-mcp/tests/helpers/test-harness.js';
|
|
5
5
|
|
|
6
6
|
vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
7
7
|
const actual = await importOriginal<typeof lib>();
|
|
@@ -11,7 +11,7 @@ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
|
|
|
11
11
|
};
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const setupHandlers = () =>
|
|
14
|
+
const setupHandlers = () => setupHandlersBase(registerExtraDocsTools);
|
|
15
15
|
|
|
16
16
|
beforeEach(() => vi.clearAllMocks());
|
|
17
17
|
|
|
@@ -518,3 +518,129 @@ describe('gog_docs_comments_delete', () => {
|
|
|
518
518
|
);
|
|
519
519
|
});
|
|
520
520
|
});
|
|
521
|
+
|
|
522
|
+
describe('gog_docs_trash', () => {
|
|
523
|
+
it('routes to gog drive delete <docId>', async () => {
|
|
524
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
525
|
+
const handlers = setupHandlers();
|
|
526
|
+
await handlers.get('gog_docs_trash')!({ docId: 'd1' });
|
|
527
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'delete', 'd1'], { account: undefined });
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('gog_docs_append', () => {
|
|
532
|
+
it('uses gog docs write --append', async () => {
|
|
533
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
534
|
+
const handlers = setupHandlers();
|
|
535
|
+
await handlers.get('gog_docs_append')!({ docId: 'd1', text: 'Hello' });
|
|
536
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
537
|
+
['docs', 'write', 'd1', '--append', '--text=Hello'],
|
|
538
|
+
{ account: undefined },
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('passes file, markdown, and tab flags', async () => {
|
|
543
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
544
|
+
const handlers = setupHandlers();
|
|
545
|
+
await handlers.get('gog_docs_append')!({
|
|
546
|
+
docId: 'd1', file: '/tmp/section.md', markdown: true, tab: 'Notes',
|
|
547
|
+
});
|
|
548
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
549
|
+
['docs', 'write', 'd1', '--append', '--file=/tmp/section.md', '--markdown', '--tab=Notes'],
|
|
550
|
+
{ account: undefined },
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe('gog_docs_read', () => {
|
|
556
|
+
it('defaults to plain text via gog docs cat', async () => {
|
|
557
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('hello'));
|
|
558
|
+
const handlers = setupHandlers();
|
|
559
|
+
await handlers.get('gog_docs_read')!({ docId: 'd1' });
|
|
560
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'cat', 'd1'], { account: undefined });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('routes json format to gog docs raw --pretty', async () => {
|
|
564
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
565
|
+
const handlers = setupHandlers();
|
|
566
|
+
await handlers.get('gog_docs_read')!({ docId: 'd1', format: 'json' });
|
|
567
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'raw', 'd1', '--pretty'], { account: undefined });
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('passes tab, allTabs, maxBytes in text mode', async () => {
|
|
571
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText(''));
|
|
572
|
+
const handlers = setupHandlers();
|
|
573
|
+
await handlers.get('gog_docs_read')!({ docId: 'd1', tab: 'Section A', allTabs: true, maxBytes: 0 });
|
|
574
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
575
|
+
['docs', 'cat', 'd1', '--tab=Section A', '--all-tabs', '--max-bytes=0'],
|
|
576
|
+
{ account: undefined },
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe('gog_docs_format', () => {
|
|
582
|
+
it('passes all text/paragraph attribute flags', async () => {
|
|
583
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
584
|
+
const handlers = setupHandlers();
|
|
585
|
+
await handlers.get('gog_docs_format')!({
|
|
586
|
+
docId: 'd1',
|
|
587
|
+
match: 'Title',
|
|
588
|
+
matchAll: true,
|
|
589
|
+
matchCase: true,
|
|
590
|
+
tab: 'Body',
|
|
591
|
+
fontFamily: 'Arial',
|
|
592
|
+
fontSize: 18,
|
|
593
|
+
textColor: '#333333',
|
|
594
|
+
bgColor: '#FFF5D9',
|
|
595
|
+
bold: true,
|
|
596
|
+
italic: true,
|
|
597
|
+
underline: true,
|
|
598
|
+
strikethrough: true,
|
|
599
|
+
alignment: 'center',
|
|
600
|
+
lineSpacing: 150,
|
|
601
|
+
});
|
|
602
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
603
|
+
[
|
|
604
|
+
'docs', 'format', 'd1',
|
|
605
|
+
'--match=Title',
|
|
606
|
+
'--match-all',
|
|
607
|
+
'--match-case',
|
|
608
|
+
'--tab=Body',
|
|
609
|
+
'--font-family=Arial',
|
|
610
|
+
'--font-size=18',
|
|
611
|
+
'--text-color=#333333',
|
|
612
|
+
'--bg-color=#FFF5D9',
|
|
613
|
+
'--bold',
|
|
614
|
+
'--italic',
|
|
615
|
+
'--underline',
|
|
616
|
+
'--strikethrough',
|
|
617
|
+
'--alignment=center',
|
|
618
|
+
'--line-spacing=150',
|
|
619
|
+
],
|
|
620
|
+
{ account: undefined },
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('emits clear-style flags (noBold/noItalic/...) without the set variants', async () => {
|
|
625
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
626
|
+
const handlers = setupHandlers();
|
|
627
|
+
await handlers.get('gog_docs_format')!({
|
|
628
|
+
docId: 'd1',
|
|
629
|
+
noBold: true,
|
|
630
|
+
noItalic: true,
|
|
631
|
+
noUnderline: true,
|
|
632
|
+
noStrikethrough: true,
|
|
633
|
+
});
|
|
634
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
635
|
+
['docs', 'format', 'd1', '--no-bold', '--no-italic', '--no-underline', '--no-strikethrough'],
|
|
636
|
+
{ account: undefined },
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('omits all flags when not provided (whole-doc no-op call passes through)', async () => {
|
|
641
|
+
vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
|
|
642
|
+
const handlers = setupHandlers();
|
|
643
|
+
await handlers.get('gog_docs_format')!({ docId: 'd1' });
|
|
644
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'format', 'd1'], { account: undefined });
|
|
645
|
+
});
|
|
646
|
+
});
|