@chrischall/gemini-mcp 2.1.3 → 2.2.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 +18 -1
- package/dist/bundle.js +810 -135
- package/dist/client.js +22 -7
- package/dist/images.js +53 -3
- package/dist/library.js +3 -2
- package/dist/storage/media.js +3 -2
- package/dist/tenant.js +93 -0
- package/dist/tools/_confirm.js +52 -28
- package/dist/tools/files.js +49 -13
- package/dist/tools/generate.js +18 -8
- package/dist/tools/interact.js +11 -5
- package/dist/tools/library.js +29 -9
- package/dist/tools/music.js +9 -4
- package/dist/tools/set.js +10 -5
- package/dist/tools/video.js +9 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/gemini-mcp/SKILL.md +3 -2
package/dist/bundle.js
CHANGED
|
@@ -69,10 +69,37 @@ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
|
69
69
|
function withCallerCapabilities(capabilities, fn) {
|
|
70
70
|
return capabilities ? storage2.run(capabilities, fn) : fn();
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
function currentCallerCapabilities() {
|
|
73
|
+
return storage2.getStore();
|
|
74
|
+
}
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
function callerCapabilities(ctx) {
|
|
79
|
+
const envelope = isRecord(ctx) && isRecord(ctx.mcpReq) ? ctx.mcpReq.envelope : void 0;
|
|
80
|
+
if (isRecord(envelope)) {
|
|
81
|
+
const declared = envelope[ENVELOPE_CAPABILITIES_KEY];
|
|
82
|
+
if (isRecord(declared))
|
|
83
|
+
return declared;
|
|
84
|
+
}
|
|
85
|
+
return currentCallerCapabilities();
|
|
86
|
+
}
|
|
87
|
+
function callerAcceptsFormElicitation(ctx) {
|
|
88
|
+
const capabilities = callerCapabilities(ctx);
|
|
89
|
+
if (!capabilities)
|
|
90
|
+
return void 0;
|
|
91
|
+
const elicitation = capabilities.elicitation;
|
|
92
|
+
if (!isRecord(elicitation))
|
|
93
|
+
return false;
|
|
94
|
+
const namedModes = ELICITATION_MODES.filter((mode) => mode in elicitation);
|
|
95
|
+
return namedModes.length === 0 || namedModes.includes("form");
|
|
96
|
+
}
|
|
97
|
+
var storage2, ENVELOPE_CAPABILITIES_KEY, ELICITATION_MODES;
|
|
73
98
|
var init_caller = __esm({
|
|
74
99
|
"node_modules/@chrischall/mcp-utils/dist/caller/index.js"() {
|
|
75
100
|
storage2 = new AsyncLocalStorage2();
|
|
101
|
+
ENVELOPE_CAPABILITIES_KEY = "io.modelcontextprotocol/clientCapabilities";
|
|
102
|
+
ELICITATION_MODES = ["form", "url"];
|
|
76
103
|
}
|
|
77
104
|
});
|
|
78
105
|
|
|
@@ -26876,6 +26903,37 @@ function buildInputRequired(spec) {
|
|
|
26876
26903
|
...spec.requestState !== void 0 && { requestState: spec.requestState }
|
|
26877
26904
|
};
|
|
26878
26905
|
}
|
|
26906
|
+
function acceptedContent(responses, key, schema) {
|
|
26907
|
+
const view = inputResponse(responses, key);
|
|
26908
|
+
if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
|
|
26909
|
+
if (schema === void 0) return view.content;
|
|
26910
|
+
const outcome = schema["~standard"].validate(view.content);
|
|
26911
|
+
if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
|
|
26912
|
+
return outcome.issues === void 0 ? outcome.value : void 0;
|
|
26913
|
+
}
|
|
26914
|
+
function inputResponse(responses, key) {
|
|
26915
|
+
if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
|
|
26916
|
+
const entry = responses[key];
|
|
26917
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
|
|
26918
|
+
const candidate = entry;
|
|
26919
|
+
if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
|
|
26920
|
+
const content = candidate["content"];
|
|
26921
|
+
return {
|
|
26922
|
+
kind: "elicit",
|
|
26923
|
+
action: candidate["action"],
|
|
26924
|
+
...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
|
|
26925
|
+
};
|
|
26926
|
+
}
|
|
26927
|
+
if (Array.isArray(candidate["roots"])) return {
|
|
26928
|
+
kind: "roots",
|
|
26929
|
+
roots: candidate["roots"]
|
|
26930
|
+
};
|
|
26931
|
+
if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
|
|
26932
|
+
kind: "sampling",
|
|
26933
|
+
result: candidate
|
|
26934
|
+
};
|
|
26935
|
+
return { kind: "missing" };
|
|
26936
|
+
}
|
|
26879
26937
|
function inputRequiredRoundsExceededMessage(method, maxRounds) {
|
|
26880
26938
|
return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
|
|
26881
26939
|
}
|
|
@@ -37659,14 +37717,14 @@ function serveStdio(factory, options = {}) {
|
|
|
37659
37717
|
return false;
|
|
37660
37718
|
};
|
|
37661
37719
|
const answerLegacyRejection = (request, reason, requestedVersion) => {
|
|
37662
|
-
const
|
|
37720
|
+
const rejection3 = modernOnlyStrictRejection({
|
|
37663
37721
|
kind: "legacy",
|
|
37664
37722
|
reason,
|
|
37665
37723
|
...requestedVersion !== void 0 && { requestedVersion }
|
|
37666
37724
|
}, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
37667
|
-
if (
|
|
37668
|
-
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${
|
|
37669
|
-
return writeErrorResponse(request.id,
|
|
37725
|
+
if (rejection3 === void 0) return Promise.resolve();
|
|
37726
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection3.cell}): ${rejection3.message}`));
|
|
37727
|
+
return writeErrorResponse(request.id, rejection3.code, rejection3.message, rejection3.data);
|
|
37670
37728
|
};
|
|
37671
37729
|
const onInstanceClosed = (channel) => {
|
|
37672
37730
|
if (closing || channel === discarding) return;
|
|
@@ -58498,6 +58556,11 @@ var init_media = __esm({
|
|
|
58498
58556
|
});
|
|
58499
58557
|
|
|
58500
58558
|
// node_modules/@chrischall/mcp-utils/dist/response/index.js
|
|
58559
|
+
function textResult(data) {
|
|
58560
|
+
return {
|
|
58561
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
58562
|
+
};
|
|
58563
|
+
}
|
|
58501
58564
|
function errorResult(message) {
|
|
58502
58565
|
return {
|
|
58503
58566
|
content: [{ type: "text", text: redactSecrets(message) }],
|
|
@@ -58512,12 +58575,446 @@ var init_response = __esm({
|
|
|
58512
58575
|
}
|
|
58513
58576
|
});
|
|
58514
58577
|
|
|
58578
|
+
// node_modules/@chrischall/mcp-utils/dist/server/canonical.js
|
|
58579
|
+
function canonicalJson(value) {
|
|
58580
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
58581
|
+
return JSON.stringify(value);
|
|
58582
|
+
if (value === void 0)
|
|
58583
|
+
return "null";
|
|
58584
|
+
if (typeof value === "number")
|
|
58585
|
+
return Number.isFinite(value) ? JSON.stringify(value) : `{"$num":"${String(value)}"}`;
|
|
58586
|
+
if (typeof value === "bigint")
|
|
58587
|
+
return `{"$bigint":"${value.toString()}"}`;
|
|
58588
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
58589
|
+
throw new TypeError(`confirmation: cannot bind a ${typeof value} argument value.`);
|
|
58590
|
+
}
|
|
58591
|
+
if (Array.isArray(value))
|
|
58592
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
58593
|
+
if (value instanceof Date) {
|
|
58594
|
+
return `{"$date":${JSON.stringify(Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString())}}`;
|
|
58595
|
+
}
|
|
58596
|
+
if (ArrayBuffer.isView(value)) {
|
|
58597
|
+
const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
58598
|
+
return `{"$bytes":${JSON.stringify(bytes.toString("base64"))}}`;
|
|
58599
|
+
}
|
|
58600
|
+
if (value instanceof ArrayBuffer)
|
|
58601
|
+
return `{"$bytes":${JSON.stringify(Buffer.from(value).toString("base64"))}}`;
|
|
58602
|
+
if (value instanceof Map) {
|
|
58603
|
+
const entries2 = [...value.entries()].map(([k, v]) => `[${canonicalJson(k)},${canonicalJson(v)}]`).sort();
|
|
58604
|
+
return `{"$map":[${entries2.join(",")}]}`;
|
|
58605
|
+
}
|
|
58606
|
+
if (value instanceof Set) {
|
|
58607
|
+
return `{"$set":[${[...value].map(canonicalJson).sort().join(",")}]}`;
|
|
58608
|
+
}
|
|
58609
|
+
const proto = Object.getPrototypeOf(value);
|
|
58610
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
58611
|
+
throw new TypeError("confirmation: cannot bind a non-plain object argument value (class instance).");
|
|
58612
|
+
}
|
|
58613
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
58614
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(escapeKey(k))}:${canonicalJson(v)}`).join(",")}}`;
|
|
58615
|
+
}
|
|
58616
|
+
function escapeKey(key) {
|
|
58617
|
+
return key.startsWith("$") ? `$${key}` : key;
|
|
58618
|
+
}
|
|
58619
|
+
var init_canonical = __esm({
|
|
58620
|
+
"node_modules/@chrischall/mcp-utils/dist/server/canonical.js"() {
|
|
58621
|
+
}
|
|
58622
|
+
});
|
|
58623
|
+
|
|
58515
58624
|
// node_modules/@chrischall/mcp-utils/dist/server/confirmation.js
|
|
58625
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
58626
|
+
function bindingKey(binding) {
|
|
58627
|
+
if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
|
|
58628
|
+
throw new RangeError("requireConfirmation: binding.ttlSeconds must be a finite number greater than 0.");
|
|
58629
|
+
}
|
|
58630
|
+
const key = typeof binding.key === "string" ? Buffer.from(binding.key, "utf8") : Buffer.from(binding.key);
|
|
58631
|
+
if (key.length < 32) {
|
|
58632
|
+
throw new RangeError("requireConfirmation: binding.key must be at least 32 bytes.");
|
|
58633
|
+
}
|
|
58634
|
+
return key;
|
|
58635
|
+
}
|
|
58636
|
+
function commitment(action, args) {
|
|
58637
|
+
return createHash("sha256").update(`${action}\0${canonicalJson(args)}`).digest("base64url");
|
|
58638
|
+
}
|
|
58639
|
+
function mintState(key, action, binding) {
|
|
58640
|
+
const exp = Math.floor(Date.now() / 1e3) + (binding.ttlSeconds ?? 600);
|
|
58641
|
+
const body = Buffer.from(JSON.stringify({ c: commitment(action, binding.args), exp })).toString("base64url");
|
|
58642
|
+
const mac3 = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest("base64url");
|
|
58643
|
+
return `${STATE_PREFIX}${body}.${mac3}`;
|
|
58644
|
+
}
|
|
58645
|
+
function verifyState(key, action, binding, state) {
|
|
58646
|
+
if (typeof state !== "string" || !state.startsWith(STATE_PREFIX))
|
|
58647
|
+
return false;
|
|
58648
|
+
const rest = state.slice(STATE_PREFIX.length);
|
|
58649
|
+
const dot = rest.indexOf(".");
|
|
58650
|
+
if (dot < 0)
|
|
58651
|
+
return false;
|
|
58652
|
+
const body = rest.slice(0, dot);
|
|
58653
|
+
const mac3 = Buffer.from(rest.slice(dot + 1), "base64url");
|
|
58654
|
+
const expected = createHmac("sha256", key).update(`${STATE_PREFIX}${body}`).digest();
|
|
58655
|
+
if (mac3.length !== expected.length || !timingSafeEqual(mac3, expected))
|
|
58656
|
+
return false;
|
|
58657
|
+
let payload;
|
|
58658
|
+
try {
|
|
58659
|
+
payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
58660
|
+
} catch {
|
|
58661
|
+
return false;
|
|
58662
|
+
}
|
|
58663
|
+
if (typeof payload.exp !== "number" || payload.exp * 1e3 <= Date.now())
|
|
58664
|
+
return false;
|
|
58665
|
+
const want = Buffer.from(commitment(action, binding.args));
|
|
58666
|
+
const got = Buffer.from(typeof payload.c === "string" ? payload.c : "");
|
|
58667
|
+
return got.length === want.length && timingSafeEqual(got, want);
|
|
58668
|
+
}
|
|
58669
|
+
function echoedState(ctx) {
|
|
58670
|
+
const accessor = ctx.mcpReq.requestState;
|
|
58671
|
+
return typeof accessor === "function" ? accessor() : void 0;
|
|
58672
|
+
}
|
|
58673
|
+
function requireConfirmation(ctx, options) {
|
|
58674
|
+
const requestKey = options.requestKey ?? DEFAULT_REQUEST_KEY;
|
|
58675
|
+
const key = options.binding ? bindingKey(options.binding) : void 0;
|
|
58676
|
+
const confirmationSchema = external_exports.object({
|
|
58677
|
+
confirmed: external_exports.boolean().describe(options.confirmationLabel ?? DEFAULT_CONFIRMATION_LABEL)
|
|
58678
|
+
});
|
|
58679
|
+
const ask = () => {
|
|
58680
|
+
const preview = {
|
|
58681
|
+
action: options.action,
|
|
58682
|
+
...options.details === void 0 ? {} : { details: options.details }
|
|
58683
|
+
};
|
|
58684
|
+
return inputRequired({
|
|
58685
|
+
inputRequests: {
|
|
58686
|
+
[requestKey]: inputRequired.elicit({
|
|
58687
|
+
message: `${options.message}
|
|
58688
|
+
${JSON.stringify(preview, null, 2)}`,
|
|
58689
|
+
requestedSchema: confirmationSchema
|
|
58690
|
+
})
|
|
58691
|
+
},
|
|
58692
|
+
...key && options.binding ? { requestState: mintState(key, options.action, options.binding) } : {}
|
|
58693
|
+
});
|
|
58694
|
+
};
|
|
58695
|
+
const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
|
|
58696
|
+
const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
|
|
58697
|
+
if (response.kind === "missing") {
|
|
58698
|
+
if (callerAcceptsFormElicitation(ctx) === false) {
|
|
58699
|
+
return textResult({
|
|
58700
|
+
confirmed: false,
|
|
58701
|
+
dispatched: false,
|
|
58702
|
+
action: options.action,
|
|
58703
|
+
reason: "confirmation-unsupported",
|
|
58704
|
+
note: options.unsupportedNote ? `${UNSUPPORTED_NOTE}. ${options.unsupportedNote}` : `${UNSUPPORTED_NOTE}.`
|
|
58705
|
+
});
|
|
58706
|
+
}
|
|
58707
|
+
return ask();
|
|
58708
|
+
}
|
|
58709
|
+
if (response.kind === "elicit" && response.action === "accept" && accepted?.confirmed === true) {
|
|
58710
|
+
if (key && options.binding) {
|
|
58711
|
+
const state = echoedState(ctx);
|
|
58712
|
+
if (state === void 0 || state === null) {
|
|
58713
|
+
return errorResult(`Confirmation for ${options.action} was accepted, but the retry carried no requestState, so it cannot be checked against the prompt that was shown. Nothing was done. The likely cause is that the MCP client or host does not round-trip requestState (it must echo the requestState from the input_required result back on the retry); asking again would loop.`);
|
|
58714
|
+
}
|
|
58715
|
+
if (!verifyState(key, options.action, options.binding, state))
|
|
58716
|
+
return ask();
|
|
58717
|
+
}
|
|
58718
|
+
return void 0;
|
|
58719
|
+
}
|
|
58720
|
+
return textResult({
|
|
58721
|
+
confirmed: false,
|
|
58722
|
+
cancelled: true,
|
|
58723
|
+
action: options.action,
|
|
58724
|
+
note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
|
|
58725
|
+
});
|
|
58726
|
+
}
|
|
58727
|
+
var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, STATE_PREFIX;
|
|
58516
58728
|
var init_confirmation = __esm({
|
|
58517
58729
|
"node_modules/@chrischall/mcp-utils/dist/server/confirmation.js"() {
|
|
58518
58730
|
init_dist();
|
|
58731
|
+
init_zod();
|
|
58732
|
+
init_caller();
|
|
58733
|
+
init_canonical();
|
|
58734
|
+
init_response();
|
|
58735
|
+
DEFAULT_REQUEST_KEY = "confirmation";
|
|
58736
|
+
DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
|
|
58737
|
+
UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
|
|
58738
|
+
STATE_PREFIX = "mcpu.confirm.v1.";
|
|
58739
|
+
}
|
|
58740
|
+
});
|
|
58741
|
+
|
|
58742
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js
|
|
58743
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
58744
|
+
function createSpentTokenStore() {
|
|
58745
|
+
const spent = /* @__PURE__ */ new Map();
|
|
58746
|
+
return {
|
|
58747
|
+
has: (nonce) => spent.has(nonce),
|
|
58748
|
+
add: (nonce, exp) => {
|
|
58749
|
+
spent.set(nonce, exp);
|
|
58750
|
+
},
|
|
58751
|
+
prune: (now) => {
|
|
58752
|
+
for (const [nonce, exp] of spent)
|
|
58753
|
+
if (exp < now)
|
|
58754
|
+
spent.delete(nonce);
|
|
58755
|
+
},
|
|
58756
|
+
clear: () => spent.clear(),
|
|
58757
|
+
get size() {
|
|
58758
|
+
return spent.size;
|
|
58759
|
+
}
|
|
58760
|
+
};
|
|
58761
|
+
}
|
|
58762
|
+
function tokenKey(key) {
|
|
58763
|
+
const bytes = typeof key === "string" ? Buffer.from(key, "utf8") : Buffer.from(key);
|
|
58764
|
+
if (bytes.length < 32)
|
|
58765
|
+
throw new RangeError("confirm token: key must be at least 32 bytes.");
|
|
58766
|
+
return bytes;
|
|
58767
|
+
}
|
|
58768
|
+
function ttlOf(ttlSeconds) {
|
|
58769
|
+
if (ttlSeconds === void 0)
|
|
58770
|
+
return DEFAULT_TTL_SECONDS;
|
|
58771
|
+
if (!(Number.isFinite(ttlSeconds) && ttlSeconds > 0)) {
|
|
58772
|
+
throw new RangeError("confirm token: ttlSeconds must be a finite number greater than 0.");
|
|
58773
|
+
}
|
|
58774
|
+
return ttlSeconds;
|
|
58775
|
+
}
|
|
58776
|
+
function sign(key, body) {
|
|
58777
|
+
return createHmac2("sha256", key).update(`${PREFIX}${body}`).digest();
|
|
58778
|
+
}
|
|
58779
|
+
function hashConfirmPayload(payload) {
|
|
58780
|
+
return createHash2("sha256").update(canonicalJson(payload)).digest("base64url");
|
|
58781
|
+
}
|
|
58782
|
+
function issueConfirmToken(key, binding, options = {}) {
|
|
58783
|
+
const k = tokenKey(key);
|
|
58784
|
+
const now = options.now ?? Date.now();
|
|
58785
|
+
const exp = now + ttlOf(options.ttlSeconds) * 1e3;
|
|
58786
|
+
const claims = {
|
|
58787
|
+
t: binding.tool,
|
|
58788
|
+
...binding.account === void 0 ? {} : { a: binding.account },
|
|
58789
|
+
g: binding.target,
|
|
58790
|
+
...binding.revision === void 0 ? {} : { r: binding.revision },
|
|
58791
|
+
h: binding.payloadHash,
|
|
58792
|
+
exp,
|
|
58793
|
+
n: randomBytes(16).toString("base64url")
|
|
58794
|
+
};
|
|
58795
|
+
const body = Buffer.from(JSON.stringify(claims), "utf8").toString("base64url");
|
|
58796
|
+
return { token: `${PREFIX}${body}.${sign(k, body).toString("base64url")}`, expiresAt: new Date(exp).toISOString() };
|
|
58797
|
+
}
|
|
58798
|
+
function parseClaims(body) {
|
|
58799
|
+
try {
|
|
58800
|
+
const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
58801
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
58802
|
+
} catch {
|
|
58803
|
+
return void 0;
|
|
58804
|
+
}
|
|
58805
|
+
}
|
|
58806
|
+
function verifyConfirmToken(key, token, binding, options = {}) {
|
|
58807
|
+
const k = tokenKey(key);
|
|
58808
|
+
const now = options.now ?? Date.now();
|
|
58809
|
+
const spent = options.spent ?? PROCESS_SPENT;
|
|
58810
|
+
spent.prune(now);
|
|
58811
|
+
if (!token.startsWith(PREFIX))
|
|
58812
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58813
|
+
const rest = token.slice(PREFIX.length);
|
|
58814
|
+
const dot = rest.indexOf(".");
|
|
58815
|
+
const body = dot < 0 ? "" : rest.slice(0, dot);
|
|
58816
|
+
const mac3 = Buffer.from(dot < 0 ? "" : rest.slice(dot + 1), "base64url");
|
|
58817
|
+
const expected = sign(k, body);
|
|
58818
|
+
if (!body || mac3.length !== expected.length || !timingSafeEqual2(mac3, expected))
|
|
58819
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58820
|
+
const claims = parseClaims(body);
|
|
58821
|
+
if (!claims)
|
|
58822
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58823
|
+
if (claims.t !== binding.tool || claims.a !== binding.account || claims.g !== binding.target) {
|
|
58824
|
+
return { ok: false, error: "TOKEN_INVALID" };
|
|
58825
|
+
}
|
|
58826
|
+
if (spent.has(claims.n))
|
|
58827
|
+
return { ok: false, error: "TOKEN_REUSED" };
|
|
58828
|
+
if (now > claims.exp)
|
|
58829
|
+
return { ok: false, error: "TOKEN_EXPIRED" };
|
|
58830
|
+
if (claims.r !== binding.revision)
|
|
58831
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "revision-changed" };
|
|
58832
|
+
if (claims.h !== binding.payloadHash)
|
|
58833
|
+
return { ok: false, error: "DRAFT_CHANGED", reason: "payload-changed" };
|
|
58834
|
+
spent.add(claims.n, claims.exp);
|
|
58835
|
+
return { ok: true };
|
|
58836
|
+
}
|
|
58837
|
+
function isToolResult(value) {
|
|
58838
|
+
return Array.isArray(value.content);
|
|
58839
|
+
}
|
|
58840
|
+
function rejection2(data) {
|
|
58841
|
+
return { ...textResult({ status: "confirmation-rejected", confirmed: false, dispatched: false, ...data }), isError: true };
|
|
58842
|
+
}
|
|
58843
|
+
async function tokenConfirmation(action, fb) {
|
|
58844
|
+
const subject = await fb.subject();
|
|
58845
|
+
if (isToolResult(subject))
|
|
58846
|
+
return subject;
|
|
58847
|
+
const binding = {
|
|
58848
|
+
tool: fb.tool,
|
|
58849
|
+
...fb.account === void 0 ? {} : { account: fb.account },
|
|
58850
|
+
target: subject.target,
|
|
58851
|
+
...subject.revision === void 0 ? {} : { revision: subject.revision },
|
|
58852
|
+
payloadHash: hashConfirmPayload(subject.payload)
|
|
58853
|
+
};
|
|
58854
|
+
const phaseOne = () => {
|
|
58855
|
+
const { token, expiresAt } = issueConfirmToken(fb.key, binding, { ttlSeconds: fb.ttlSeconds });
|
|
58856
|
+
return {
|
|
58857
|
+
action,
|
|
58858
|
+
preview: subject.preview,
|
|
58859
|
+
confirmToken: token,
|
|
58860
|
+
expiresAt,
|
|
58861
|
+
ttlSeconds: ttlOf(fb.ttlSeconds),
|
|
58862
|
+
instruction: fb.instruction ?? CONFIRM_TOKEN_INSTRUCTION
|
|
58863
|
+
};
|
|
58864
|
+
};
|
|
58865
|
+
if (!fb.confirmToken) {
|
|
58866
|
+
return textResult({ status: "confirmation-required", confirmed: false, dispatched: false, ...phaseOne() });
|
|
58867
|
+
}
|
|
58868
|
+
const verdict = verifyConfirmToken(fb.key, fb.confirmToken, binding, { spent: fb.spent });
|
|
58869
|
+
if (verdict.ok)
|
|
58870
|
+
return void 0;
|
|
58871
|
+
if (verdict.error === "DRAFT_CHANGED") {
|
|
58872
|
+
return rejection2({
|
|
58873
|
+
error: "DRAFT_CHANGED",
|
|
58874
|
+
reason: verdict.reason,
|
|
58875
|
+
note: `${CHANGED_NOTE[verdict.reason]} The current preview and a fresh confirmToken are below.`,
|
|
58876
|
+
...phaseOne()
|
|
58877
|
+
});
|
|
58878
|
+
}
|
|
58879
|
+
return rejection2({ error: verdict.error, action, note: TOKEN_ERROR_NOTE[verdict.error] });
|
|
58880
|
+
}
|
|
58881
|
+
async function requireConfirmationWithFallback(ctx, options) {
|
|
58882
|
+
const { tokenFallback, ...confirmation } = options;
|
|
58883
|
+
if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
|
|
58884
|
+
return tokenConfirmation(options.action, tokenFallback);
|
|
58885
|
+
}
|
|
58886
|
+
return requireConfirmation(ctx, confirmation);
|
|
58887
|
+
}
|
|
58888
|
+
var PROCESS_SPENT, PREFIX, DEFAULT_TTL_SECONDS, CONFIRM_TOKEN_INSTRUCTION, confirmTokenParam, TOKEN_ERROR_NOTE, CHANGED_NOTE;
|
|
58889
|
+
var init_confirm_token = __esm({
|
|
58890
|
+
"node_modules/@chrischall/mcp-utils/dist/server/confirm-token.js"() {
|
|
58891
|
+
init_zod();
|
|
58519
58892
|
init_caller();
|
|
58520
58893
|
init_response();
|
|
58894
|
+
init_canonical();
|
|
58895
|
+
init_confirmation();
|
|
58896
|
+
PROCESS_SPENT = createSpentTokenStore();
|
|
58897
|
+
PREFIX = "mcpu.token.v1.";
|
|
58898
|
+
DEFAULT_TTL_SECONDS = 600;
|
|
58899
|
+
CONFIRM_TOKEN_INSTRUCTION = "Show this preview to the user verbatim and proceed only after they explicitly approve in chat. Then call again with confirmToken.";
|
|
58900
|
+
confirmTokenParam = external_exports.string().optional().describe(`ONLY for the two-step confirmation fallback (a client without MCP elicitation). The confirmToken from this same tool's phase-1 "confirmation-required" response, passed back ONLY after the user has seen that preview and explicitly approved it in chat \u2014 never on the first call, never invented, never reused. Call again with the same arguments. Ignored when the client supports elicitation.`);
|
|
58901
|
+
TOKEN_ERROR_NOTE = {
|
|
58902
|
+
TOKEN_EXPIRED: "Nothing was sent or changed: the confirmToken expired. Call again WITHOUT confirmToken for a fresh preview, and ask the user to approve it again.",
|
|
58903
|
+
TOKEN_REUSED: "Nothing was sent or changed by this call: this confirmToken was already used, and one approval acts once. If doing it again is really intended, call again WITHOUT confirmToken and get a new approval.",
|
|
58904
|
+
TOKEN_INVALID: "Nothing was sent or changed: this confirmToken was not issued by this server for this tool, account and target (or the server has restarted since). Call again WITHOUT confirmToken for a fresh preview and approval."
|
|
58905
|
+
};
|
|
58906
|
+
CHANGED_NOTE = {
|
|
58907
|
+
"revision-changed": "Nothing was sent or changed: the target was edited since the user approved it (its version rotated), so what would happen is not what they saw.",
|
|
58908
|
+
"payload-changed": "Nothing was sent or changed: what would happen no longer matches what the user approved."
|
|
58909
|
+
};
|
|
58910
|
+
}
|
|
58911
|
+
});
|
|
58912
|
+
|
|
58913
|
+
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
58914
|
+
import { homedir } from "node:os";
|
|
58915
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
58916
|
+
function readEnvVar(key, opts = {}) {
|
|
58917
|
+
const env = opts.env ?? process.env;
|
|
58918
|
+
const raw = env[key];
|
|
58919
|
+
if (typeof raw === "string") {
|
|
58920
|
+
const trimmed = raw.trim();
|
|
58921
|
+
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
58922
|
+
return trimmed;
|
|
58923
|
+
}
|
|
58924
|
+
}
|
|
58925
|
+
return opts.default;
|
|
58926
|
+
}
|
|
58927
|
+
function expandPath(p) {
|
|
58928
|
+
let expanded = p;
|
|
58929
|
+
if (p === "~") {
|
|
58930
|
+
expanded = homedir();
|
|
58931
|
+
} else if (p.startsWith("~/")) {
|
|
58932
|
+
expanded = join(homedir(), p.slice(2));
|
|
58933
|
+
}
|
|
58934
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
58935
|
+
}
|
|
58936
|
+
async function loadDotenvSafely(opts = {}) {
|
|
58937
|
+
try {
|
|
58938
|
+
const mod = await import(
|
|
58939
|
+
/* @vite-ignore */
|
|
58940
|
+
"dotenv"
|
|
58941
|
+
);
|
|
58942
|
+
const result = mod.config({
|
|
58943
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
58944
|
+
override: opts.override ?? false,
|
|
58945
|
+
quiet: true
|
|
58946
|
+
});
|
|
58947
|
+
return result.error === void 0;
|
|
58948
|
+
} catch {
|
|
58949
|
+
return false;
|
|
58950
|
+
}
|
|
58951
|
+
}
|
|
58952
|
+
var PLACEHOLDER_RE;
|
|
58953
|
+
var init_config = __esm({
|
|
58954
|
+
"node_modules/@chrischall/mcp-utils/dist/config/index.js"() {
|
|
58955
|
+
PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
58956
|
+
}
|
|
58957
|
+
});
|
|
58958
|
+
|
|
58959
|
+
// node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js
|
|
58960
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "node:crypto";
|
|
58961
|
+
function readConfirmMode(env = process.env) {
|
|
58962
|
+
const raw = readEnvVar("MCP_CONFIRM_MODE", { env })?.trim().toLowerCase();
|
|
58963
|
+
if (!raw)
|
|
58964
|
+
return "ask-user";
|
|
58965
|
+
if (MODES.has(raw))
|
|
58966
|
+
return raw;
|
|
58967
|
+
if (!warned.has(raw)) {
|
|
58968
|
+
warned.add(raw);
|
|
58969
|
+
process.stderr.write(`MCP_CONFIRM_MODE="${raw}" is not one of ask-user, auto, refuse; treating it as refuse.
|
|
58970
|
+
`);
|
|
58971
|
+
}
|
|
58972
|
+
return "refuse";
|
|
58973
|
+
}
|
|
58974
|
+
function confirmTtlFromEnv(env = process.env) {
|
|
58975
|
+
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
58976
|
+
return raw && /^[1-9]\d*$/.test(raw) ? Number(raw) : DEFAULT_TTL_SECONDS2;
|
|
58977
|
+
}
|
|
58978
|
+
function confirmKeyFromEnv(env = process.env) {
|
|
58979
|
+
const secret = readEnvVar("MCP_CONFIRM_SECRET", { env });
|
|
58980
|
+
if (secret)
|
|
58981
|
+
return createHash3("sha256").update(secret, "utf8").digest();
|
|
58982
|
+
processKey ??= randomBytes2(32);
|
|
58983
|
+
return processKey;
|
|
58984
|
+
}
|
|
58985
|
+
function confirmationFromEnv(options) {
|
|
58986
|
+
const { tool, account, confirmToken, subject, instruction, spent, env = process.env, ...confirmation } = options;
|
|
58987
|
+
const mode = readConfirmMode(env);
|
|
58988
|
+
if (mode === "refuse") {
|
|
58989
|
+
return {
|
|
58990
|
+
...confirmation,
|
|
58991
|
+
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${REFUSE_HINT}` : REFUSE_HINT
|
|
58992
|
+
};
|
|
58993
|
+
}
|
|
58994
|
+
return {
|
|
58995
|
+
...confirmation,
|
|
58996
|
+
tokenFallback: {
|
|
58997
|
+
key: confirmKeyFromEnv(env),
|
|
58998
|
+
tool,
|
|
58999
|
+
...account === void 0 ? {} : { account },
|
|
59000
|
+
...confirmToken === void 0 ? {} : { confirmToken },
|
|
59001
|
+
subject,
|
|
59002
|
+
ttlSeconds: confirmTtlFromEnv(env),
|
|
59003
|
+
instruction: mode === "auto" ? CONFIRM_TOKEN_AUTO_INSTRUCTION : instruction ?? CONFIRM_TOKEN_INSTRUCTION,
|
|
59004
|
+
...spent === void 0 ? {} : { spent }
|
|
59005
|
+
}
|
|
59006
|
+
};
|
|
59007
|
+
}
|
|
59008
|
+
var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT;
|
|
59009
|
+
var init_confirm_env = __esm({
|
|
59010
|
+
"node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js"() {
|
|
59011
|
+
init_config();
|
|
59012
|
+
init_confirm_token();
|
|
59013
|
+
MODES = /* @__PURE__ */ new Set(["ask-user", "auto", "refuse"]);
|
|
59014
|
+
DEFAULT_TTL_SECONDS2 = 600;
|
|
59015
|
+
warned = /* @__PURE__ */ new Set();
|
|
59016
|
+
CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
|
|
59017
|
+
REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
|
|
58521
59018
|
}
|
|
58522
59019
|
});
|
|
58523
59020
|
|
|
@@ -58697,6 +59194,8 @@ var init_server = __esm({
|
|
|
58697
59194
|
init_errors();
|
|
58698
59195
|
init_response();
|
|
58699
59196
|
init_confirmation();
|
|
59197
|
+
init_confirm_token();
|
|
59198
|
+
init_confirm_env();
|
|
58700
59199
|
init_cancel();
|
|
58701
59200
|
init_caller();
|
|
58702
59201
|
SERVER_PROTOCOL_VERSIONS = Object.freeze([
|
|
@@ -58708,52 +59207,6 @@ var init_server = __esm({
|
|
|
58708
59207
|
}
|
|
58709
59208
|
});
|
|
58710
59209
|
|
|
58711
|
-
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
58712
|
-
import { homedir } from "node:os";
|
|
58713
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
58714
|
-
function readEnvVar(key, opts = {}) {
|
|
58715
|
-
const env = opts.env ?? process.env;
|
|
58716
|
-
const raw = env[key];
|
|
58717
|
-
if (typeof raw === "string") {
|
|
58718
|
-
const trimmed = raw.trim();
|
|
58719
|
-
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
58720
|
-
return trimmed;
|
|
58721
|
-
}
|
|
58722
|
-
}
|
|
58723
|
-
return opts.default;
|
|
58724
|
-
}
|
|
58725
|
-
function expandPath(p) {
|
|
58726
|
-
let expanded = p;
|
|
58727
|
-
if (p === "~") {
|
|
58728
|
-
expanded = homedir();
|
|
58729
|
-
} else if (p.startsWith("~/")) {
|
|
58730
|
-
expanded = join(homedir(), p.slice(2));
|
|
58731
|
-
}
|
|
58732
|
-
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
58733
|
-
}
|
|
58734
|
-
async function loadDotenvSafely(opts = {}) {
|
|
58735
|
-
try {
|
|
58736
|
-
const mod = await import(
|
|
58737
|
-
/* @vite-ignore */
|
|
58738
|
-
"dotenv"
|
|
58739
|
-
);
|
|
58740
|
-
const result = mod.config({
|
|
58741
|
-
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
58742
|
-
override: opts.override ?? false,
|
|
58743
|
-
quiet: true
|
|
58744
|
-
});
|
|
58745
|
-
return result.error === void 0;
|
|
58746
|
-
} catch {
|
|
58747
|
-
return false;
|
|
58748
|
-
}
|
|
58749
|
-
}
|
|
58750
|
-
var PLACEHOLDER_RE;
|
|
58751
|
-
var init_config = __esm({
|
|
58752
|
-
"node_modules/@chrischall/mcp-utils/dist/config/index.js"() {
|
|
58753
|
-
PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
58754
|
-
}
|
|
58755
|
-
});
|
|
58756
|
-
|
|
58757
59210
|
// node_modules/@chrischall/mcp-utils/dist/fs/confine.js
|
|
58758
59211
|
import { existsSync, realpathSync } from "node:fs";
|
|
58759
59212
|
import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
|
|
@@ -59233,6 +59686,7 @@ var images_exports = {};
|
|
|
59233
59686
|
__export(images_exports, {
|
|
59234
59687
|
baseName: () => baseName,
|
|
59235
59688
|
decodeImageInput: () => decodeImageInput,
|
|
59689
|
+
detectUploadMime: () => detectUploadMime,
|
|
59236
59690
|
mediaExt: () => mediaExt,
|
|
59237
59691
|
previewImageInput: () => previewImageInput,
|
|
59238
59692
|
previewVideoInput: () => previewVideoInput,
|
|
@@ -59272,6 +59726,9 @@ async function uniquePath(dir, base, ext) {
|
|
|
59272
59726
|
return candidate;
|
|
59273
59727
|
}
|
|
59274
59728
|
function sniffMimeBytes(buf) {
|
|
59729
|
+
return sniffImageMime(buf) ?? "image/png";
|
|
59730
|
+
}
|
|
59731
|
+
function sniffImageMime(buf) {
|
|
59275
59732
|
if (buf.length >= 4 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
|
|
59276
59733
|
return "image/png";
|
|
59277
59734
|
}
|
|
@@ -59281,7 +59738,7 @@ function sniffMimeBytes(buf) {
|
|
|
59281
59738
|
if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
|
|
59282
59739
|
return "image/webp";
|
|
59283
59740
|
}
|
|
59284
|
-
return
|
|
59741
|
+
return void 0;
|
|
59285
59742
|
}
|
|
59286
59743
|
function mediaExt(mimeType) {
|
|
59287
59744
|
const m = mimeType.toLowerCase();
|
|
@@ -59356,6 +59813,22 @@ async function previewVideoInput(path) {
|
|
|
59356
59813
|
const { size } = await stat(resolved);
|
|
59357
59814
|
return { path: resolved, mimeType: videoMimeType(resolved), size };
|
|
59358
59815
|
}
|
|
59816
|
+
async function readHead(path, n = 16) {
|
|
59817
|
+
const fh = await open2(path, "r");
|
|
59818
|
+
try {
|
|
59819
|
+
const head = Buffer.alloc(n);
|
|
59820
|
+
const { bytesRead } = await fh.read(head, 0, n, 0);
|
|
59821
|
+
return head.subarray(0, bytesRead);
|
|
59822
|
+
} finally {
|
|
59823
|
+
await fh.close();
|
|
59824
|
+
}
|
|
59825
|
+
}
|
|
59826
|
+
async function detectUploadMime(resolvedPath) {
|
|
59827
|
+
const sniffed = sniffImageMime(await readHead(resolvedPath));
|
|
59828
|
+
if (sniffed) return sniffed;
|
|
59829
|
+
const ext = resolvedPath.includes(".") ? resolvedPath.slice(resolvedPath.lastIndexOf(".") + 1).toLowerCase() : "";
|
|
59830
|
+
return UPLOAD_MIME_BY_EXT[ext];
|
|
59831
|
+
}
|
|
59359
59832
|
async function readImageAsInline(path) {
|
|
59360
59833
|
const resolved = resolveImagePath(path);
|
|
59361
59834
|
const buf = await readFile(resolved);
|
|
@@ -59383,7 +59856,7 @@ function baseName(name) {
|
|
|
59383
59856
|
function resolveOutputDir(perCall) {
|
|
59384
59857
|
return perCall?.trim() || readEnvVar("GEMINI_OUTPUT_DIR") || process.cwd();
|
|
59385
59858
|
}
|
|
59386
|
-
var VIDEO_MIME_BY_EXT;
|
|
59859
|
+
var VIDEO_MIME_BY_EXT, UPLOAD_MIME_BY_EXT;
|
|
59387
59860
|
var init_images = __esm({
|
|
59388
59861
|
"src/images.ts"() {
|
|
59389
59862
|
"use strict";
|
|
@@ -59400,6 +59873,23 @@ var init_images = __esm({
|
|
|
59400
59873
|
"3gp": "video/3gpp",
|
|
59401
59874
|
"3gpp": "video/3gpp"
|
|
59402
59875
|
};
|
|
59876
|
+
UPLOAD_MIME_BY_EXT = {
|
|
59877
|
+
png: "image/png",
|
|
59878
|
+
jpg: "image/jpeg",
|
|
59879
|
+
jpeg: "image/jpeg",
|
|
59880
|
+
webp: "image/webp",
|
|
59881
|
+
gif: "image/gif",
|
|
59882
|
+
heic: "image/heic",
|
|
59883
|
+
heif: "image/heif",
|
|
59884
|
+
wav: "audio/wav",
|
|
59885
|
+
mp3: "audio/mp3",
|
|
59886
|
+
aiff: "audio/aiff",
|
|
59887
|
+
aif: "audio/aiff",
|
|
59888
|
+
aac: "audio/aac",
|
|
59889
|
+
ogg: "audio/ogg",
|
|
59890
|
+
flac: "audio/flac",
|
|
59891
|
+
...VIDEO_MIME_BY_EXT
|
|
59892
|
+
};
|
|
59403
59893
|
}
|
|
59404
59894
|
});
|
|
59405
59895
|
|
|
@@ -59476,7 +59966,7 @@ async function loadStdioDotenv() {
|
|
|
59476
59966
|
}
|
|
59477
59967
|
|
|
59478
59968
|
// src/version.ts
|
|
59479
|
-
var VERSION = "2.
|
|
59969
|
+
var VERSION = "2.2.0";
|
|
59480
59970
|
|
|
59481
59971
|
// src/errors.ts
|
|
59482
59972
|
init_dist2();
|
|
@@ -60087,11 +60577,11 @@ async function resolveImageInputs(args, client2) {
|
|
|
60087
60577
|
async function resolveLocalPath(path, client2, onDisk, report) {
|
|
60088
60578
|
const { readImageAsInline: readImageAsInline2, resolveImagePath: resolveImagePath2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
60089
60579
|
if (!onDisk) return readImageAsInline2(path);
|
|
60090
|
-
const { stat:
|
|
60580
|
+
const { stat: stat4 } = await import("node:fs/promises");
|
|
60091
60581
|
const resolved = resolveImagePath2(path);
|
|
60092
60582
|
let key;
|
|
60093
60583
|
try {
|
|
60094
|
-
const { mtimeMs, size } = await
|
|
60584
|
+
const { mtimeMs, size } = await stat4(resolved);
|
|
60095
60585
|
key = `path\0${resolved}\0${mtimeMs}\0${size}`;
|
|
60096
60586
|
} catch {
|
|
60097
60587
|
return readImageAsInline2(path);
|
|
@@ -60194,6 +60684,61 @@ function signedLinks(baseUrl) {
|
|
|
60194
60684
|
};
|
|
60195
60685
|
}
|
|
60196
60686
|
|
|
60687
|
+
// src/tenant.ts
|
|
60688
|
+
var TENANT_PIN_KEY = "lib/tenant.json";
|
|
60689
|
+
var TENANT_SHAPE = /^[0-9a-f]{12}$/;
|
|
60690
|
+
async function readPin(bucket) {
|
|
60691
|
+
const object4 = await bucket.get(TENANT_PIN_KEY);
|
|
60692
|
+
if (!object4) return void 0;
|
|
60693
|
+
try {
|
|
60694
|
+
const parsed = JSON.parse(new TextDecoder().decode(await object4.arrayBuffer()));
|
|
60695
|
+
return typeof parsed.tenant === "string" && TENANT_SHAPE.test(parsed.tenant) ? parsed.tenant : void 0;
|
|
60696
|
+
} catch {
|
|
60697
|
+
return void 0;
|
|
60698
|
+
}
|
|
60699
|
+
}
|
|
60700
|
+
function pinnedTenant(bucket, derive) {
|
|
60701
|
+
let cached2;
|
|
60702
|
+
async function resolve4() {
|
|
60703
|
+
const pinned = await readPin(bucket);
|
|
60704
|
+
if (pinned) return { tenant: pinned, pinned: true };
|
|
60705
|
+
const tenant = await derive();
|
|
60706
|
+
try {
|
|
60707
|
+
await bucket.put(TENANT_PIN_KEY, JSON.stringify({ tenant }), {
|
|
60708
|
+
httpMetadata: { contentType: "application/json" }
|
|
60709
|
+
});
|
|
60710
|
+
return { tenant, pinned: true };
|
|
60711
|
+
} catch {
|
|
60712
|
+
return { tenant, pinned: false };
|
|
60713
|
+
}
|
|
60714
|
+
}
|
|
60715
|
+
return () => {
|
|
60716
|
+
if (cached2) return cached2;
|
|
60717
|
+
const attempt = resolve4().then(({ tenant, pinned }) => {
|
|
60718
|
+
if (!pinned && cached2 === attempt) cached2 = void 0;
|
|
60719
|
+
return tenant;
|
|
60720
|
+
});
|
|
60721
|
+
cached2 = attempt;
|
|
60722
|
+
attempt.catch(() => {
|
|
60723
|
+
if (cached2 === attempt) cached2 = void 0;
|
|
60724
|
+
});
|
|
60725
|
+
return attempt;
|
|
60726
|
+
};
|
|
60727
|
+
}
|
|
60728
|
+
function tenantResolver(source) {
|
|
60729
|
+
let cache;
|
|
60730
|
+
return () => {
|
|
60731
|
+
if (cache) return cache;
|
|
60732
|
+
const attempt = Promise.resolve(
|
|
60733
|
+
typeof source === "function" ? source() : source
|
|
60734
|
+
);
|
|
60735
|
+
attempt.catch(() => {
|
|
60736
|
+
if (cache === attempt) cache = void 0;
|
|
60737
|
+
});
|
|
60738
|
+
return cache = attempt;
|
|
60739
|
+
};
|
|
60740
|
+
}
|
|
60741
|
+
|
|
60197
60742
|
// src/storage/media.ts
|
|
60198
60743
|
function createDiskSink() {
|
|
60199
60744
|
return {
|
|
@@ -60251,8 +60796,7 @@ function createR2Sink(bucket, opts) {
|
|
|
60251
60796
|
const signedBase = opts.links?.base ?? opts.signedBaseUrl?.replace(/\/+$/, "");
|
|
60252
60797
|
const ttlMs = opts.urlTtlMs ?? 0;
|
|
60253
60798
|
const readPrefixes = [prefix, ...opts.readPrefixes ?? []];
|
|
60254
|
-
|
|
60255
|
-
const tenantId = () => tenantCache ??= Promise.resolve(typeof opts.tenant === "function" ? opts.tenant() : opts.tenant);
|
|
60799
|
+
const tenantId = tenantResolver(opts.tenant);
|
|
60256
60800
|
const ownPrefix = async () => {
|
|
60257
60801
|
const tenant = await tenantId();
|
|
60258
60802
|
return tenant ? `${prefix}/${tenant}/` : `${prefix}/`;
|
|
@@ -60851,21 +61395,21 @@ function assertLocalInputsAvailable(sink, args) {
|
|
|
60851
61395
|
|
|
60852
61396
|
// src/jobs.ts
|
|
60853
61397
|
init_dist2();
|
|
60854
|
-
import { randomUUID, createHash as
|
|
61398
|
+
import { randomUUID, createHash as createHash5 } from "node:crypto";
|
|
60855
61399
|
|
|
60856
61400
|
// src/job-store.ts
|
|
60857
|
-
import { createHash } from "node:crypto";
|
|
61401
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
60858
61402
|
var HEARTBEAT_MS = 6e4;
|
|
60859
61403
|
var EXECUTOR_LOST_AFTER_MS = 3 * HEARTBEAT_MS;
|
|
60860
|
-
var
|
|
61404
|
+
var PREFIX2 = "jobs";
|
|
60861
61405
|
function keyHash(idempotencyKey) {
|
|
60862
|
-
return
|
|
61406
|
+
return createHash4("sha256").update(idempotencyKey).digest("hex").slice(0, 32);
|
|
60863
61407
|
}
|
|
60864
61408
|
function createBlobJobStore(bucket, opts) {
|
|
60865
61409
|
const now = opts.now ?? (() => Date.now());
|
|
60866
61410
|
const tenantOf = async () => typeof opts.tenant === "function" ? opts.tenant() : opts.tenant;
|
|
60867
|
-
const recordKey = async (jobId) => `${
|
|
60868
|
-
const pointerKey = async (k) => `${
|
|
61411
|
+
const recordKey = async (jobId) => `${PREFIX2}/${await tenantOf()}/${encodeURIComponent(jobId)}.json`;
|
|
61412
|
+
const pointerKey = async (k) => `${PREFIX2}/${await tenantOf()}/keys/${keyHash(k)}.json`;
|
|
60869
61413
|
async function writeJson(key, value) {
|
|
60870
61414
|
try {
|
|
60871
61415
|
await bucket.put(key, JSON.stringify(value), { httpMetadata: { contentType: "application/json" } });
|
|
@@ -61009,7 +61553,7 @@ function isExpired(entry, now) {
|
|
|
61009
61553
|
return entry.settledAt !== void 0 && now - entry.settledAt > JOB_TTL_MS;
|
|
61010
61554
|
}
|
|
61011
61555
|
function fingerprintRequest(toolName, parts) {
|
|
61012
|
-
return
|
|
61556
|
+
return createHash5("sha256").update(`${toolName}\0${JSON.stringify(parts)}`).digest("hex");
|
|
61013
61557
|
}
|
|
61014
61558
|
function replayKey(idempotencyKey, fingerprint) {
|
|
61015
61559
|
return `${idempotencyKey}\0${fingerprint}`;
|
|
@@ -61602,24 +62146,43 @@ function attachCost(meta3, model, usage) {
|
|
|
61602
62146
|
// src/tools/_confirm.ts
|
|
61603
62147
|
init_dist2();
|
|
61604
62148
|
init_images();
|
|
61605
|
-
function
|
|
61606
|
-
|
|
61607
|
-
|
|
61608
|
-
|
|
61609
|
-
|
|
61610
|
-
|
|
61611
|
-
|
|
61612
|
-
|
|
61613
|
-
|
|
61614
|
-
}
|
|
62149
|
+
function confirmNote(lead = "Asks the user to confirm first") {
|
|
62150
|
+
return `${lead}: a confirmation prompt where the client supports one; otherwise the first call returns a preview and a confirmToken, and only a repeat call with that token proceeds (see MCP_CONFIRM_MODE).`;
|
|
62151
|
+
}
|
|
62152
|
+
async function confirmWrite(ctx, o) {
|
|
62153
|
+
const preview = {
|
|
62154
|
+
action: o.description,
|
|
62155
|
+
method: o.method,
|
|
62156
|
+
path: o.path,
|
|
62157
|
+
...o.body !== void 0 ? { willSend: o.body } : {}
|
|
62158
|
+
};
|
|
62159
|
+
return requireConfirmationWithFallback(ctx, confirmationFromEnv({
|
|
62160
|
+
action: o.action,
|
|
62161
|
+
message: o.message,
|
|
62162
|
+
details: preview,
|
|
62163
|
+
tool: o.tool,
|
|
62164
|
+
confirmToken: o.confirmToken,
|
|
62165
|
+
subject: () => ({ target: o.target, payload: o.payload, preview })
|
|
62166
|
+
}));
|
|
61615
62167
|
}
|
|
61616
|
-
async function
|
|
61617
|
-
const hasLocal = (imagePaths?.length ?? 0) > 0 || Boolean(videoPath);
|
|
61618
|
-
if (
|
|
62168
|
+
async function confirmLocalInputs(ctx, o) {
|
|
62169
|
+
const hasLocal = (o.imagePaths?.length ?? 0) > 0 || Boolean(o.videoPath);
|
|
62170
|
+
if (!hasLocal) return void 0;
|
|
61619
62171
|
const inputs = [];
|
|
61620
|
-
for (const p of imagePaths ?? []) inputs.push(await previewImageInput(p));
|
|
61621
|
-
if (videoPath) inputs.push(await previewVideoInput(videoPath));
|
|
61622
|
-
return
|
|
62172
|
+
for (const p of o.imagePaths ?? []) inputs.push(await previewImageInput(p));
|
|
62173
|
+
if (o.videoPath) inputs.push(await previewVideoInput(o.videoPath));
|
|
62174
|
+
return confirmWrite(ctx, {
|
|
62175
|
+
tool: o.tool,
|
|
62176
|
+
action: o.action,
|
|
62177
|
+
message: "Review and confirm sending these local files to Google:",
|
|
62178
|
+
description: o.description,
|
|
62179
|
+
method: "POST",
|
|
62180
|
+
path: o.endpoint,
|
|
62181
|
+
body: { inputs },
|
|
62182
|
+
target: "",
|
|
62183
|
+
payload: { request: o.request, inputs },
|
|
62184
|
+
confirmToken: o.confirmToken
|
|
62185
|
+
});
|
|
61623
62186
|
}
|
|
61624
62187
|
|
|
61625
62188
|
// src/tools/generate.ts
|
|
@@ -61630,7 +62193,7 @@ function registerGenerateTools(server, client2) {
|
|
|
61630
62193
|
server.registerTool(
|
|
61631
62194
|
"gemini_image_generate",
|
|
61632
62195
|
{
|
|
61633
|
-
description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point.",
|
|
62196
|
+
description: "Generate image(s) from a text prompt with a Gemini image model (Nano Banana / Nano Banana Pro). If the result will likely be refined iteratively, prefer gemini_interact (multi-turn) as the entry point. " + confirmNote("Local file inputs are confirmed first"),
|
|
61634
62197
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
61635
62198
|
inputSchema: external_exports.object({
|
|
61636
62199
|
prompt: external_exports.string().min(1).describe("Text prompt describing the image"),
|
|
@@ -61645,13 +62208,23 @@ function registerGenerateTools(server, client2) {
|
|
|
61645
62208
|
style: styleSchema,
|
|
61646
62209
|
video_url: external_exports.string().url().optional().describe("Public YouTube URL (or a previously uploaded Files API uri) as a video reference (video\u2192image; use a Flash model e.g. gemini-3.1-flash-image)"),
|
|
61647
62210
|
video_path: videoPathSchema,
|
|
61648
|
-
|
|
62211
|
+
confirmToken: confirmTokenParam,
|
|
61649
62212
|
...sharedImageSchema
|
|
61650
62213
|
})
|
|
61651
62214
|
},
|
|
61652
62215
|
async (args, extra) => {
|
|
61653
62216
|
assertLocalInputsAvailable(client2.mediaSink, args);
|
|
61654
|
-
const
|
|
62217
|
+
const { confirmToken, ...request } = args;
|
|
62218
|
+
const gate = await confirmLocalInputs(extra, {
|
|
62219
|
+
tool: "gemini_image_generate",
|
|
62220
|
+
action: "image.generate",
|
|
62221
|
+
description: SEND_LOCAL_ACTION,
|
|
62222
|
+
endpoint: GENERATE_ENDPOINT,
|
|
62223
|
+
imagePaths: args.images,
|
|
62224
|
+
videoPath: args.video_path,
|
|
62225
|
+
request,
|
|
62226
|
+
confirmToken
|
|
62227
|
+
});
|
|
61655
62228
|
if (gate) return gate;
|
|
61656
62229
|
const count = args.count ?? 1;
|
|
61657
62230
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
@@ -61740,7 +62313,7 @@ function registerGenerateTools(server, client2) {
|
|
|
61740
62313
|
server.registerTool(
|
|
61741
62314
|
"gemini_image_edit",
|
|
61742
62315
|
{
|
|
61743
|
-
description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. For a SERIES of successive edits to the same image, prefer gemini_interact (multi-turn) \u2014 it keeps edit context and avoids re-processing the full image each round; use gemini_image_edit for one-off edits or composing multiple distinct inputs. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording.",
|
|
62316
|
+
description: "Edit or compose images: provide one or more input images (paths or base64), plus a text instruction. For a SERIES of successive edits to the same image, prefer gemini_interact (multi-turn) \u2014 it keeps edit context and avoids re-processing the full image each round; use gemini_image_edit for one-off edits or composing multiple distinct inputs. Gemini over-preserves the input; there is no edit-strength control \u2014 for large structural changes, reroll with a different `seed` or more forceful wording. " + confirmNote("Local file inputs are confirmed first"),
|
|
61744
62317
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
61745
62318
|
inputSchema: external_exports.object({
|
|
61746
62319
|
prompt: external_exports.string().min(1).describe("Instruction describing the edit or composition"),
|
|
@@ -61752,14 +62325,23 @@ function registerGenerateTools(server, client2) {
|
|
|
61752
62325
|
characters: charactersSchema,
|
|
61753
62326
|
style: styleSchema,
|
|
61754
62327
|
filename: external_exports.string().optional().describe("Base filename for the output image (extension stripped; default: slugified prompt)"),
|
|
61755
|
-
|
|
62328
|
+
confirmToken: confirmTokenParam,
|
|
61756
62329
|
...sharedImageSchema
|
|
61757
62330
|
})
|
|
61758
62331
|
},
|
|
61759
62332
|
async (args, extra) => {
|
|
61760
62333
|
assertLocalInputsAvailable(client2.mediaSink, args);
|
|
61761
62334
|
if (!args.characters?.length) requireImageInput(args);
|
|
61762
|
-
const
|
|
62335
|
+
const { confirmToken, ...request } = args;
|
|
62336
|
+
const gate = await confirmLocalInputs(extra, {
|
|
62337
|
+
tool: "gemini_image_edit",
|
|
62338
|
+
action: "image.edit",
|
|
62339
|
+
description: SEND_LOCAL_ACTION,
|
|
62340
|
+
endpoint: GENERATE_ENDPOINT,
|
|
62341
|
+
imagePaths: args.images,
|
|
62342
|
+
request,
|
|
62343
|
+
confirmToken
|
|
62344
|
+
});
|
|
61763
62345
|
if (gate) return gate;
|
|
61764
62346
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
61765
62347
|
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
@@ -61822,7 +62404,7 @@ function registerSetTools(server, client2) {
|
|
|
61822
62404
|
server.registerTool(
|
|
61823
62405
|
"gemini_image_set",
|
|
61824
62406
|
{
|
|
61825
|
-
description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long.',
|
|
62407
|
+
description: 'Generate a consistent SET of images: a master image from master_prompt, then one image per scene that references the master so the subject/style stays consistent. Provide `scenes` (explicit per-image prompts) OR `count` (variations of the master). Scene generations run in parallel (reference_mode "master", the default). On the hosted connector: saved `characters` and a saved `style` can seed the whole set by name, multi-image results include a `bundle_url` zip of every image (one curl instead of N), and `max_wait_ms` returns a pollable job handle if the batch runs long. ' + confirmNote("Local file inputs are confirmed first"),
|
|
61826
62408
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
61827
62409
|
inputSchema: external_exports.object({
|
|
61828
62410
|
master_prompt: external_exports.string().min(1).describe("Prompt for the master/reference image"),
|
|
@@ -61837,7 +62419,7 @@ function registerSetTools(server, client2) {
|
|
|
61837
62419
|
characters: charactersSchema,
|
|
61838
62420
|
style: styleSchema,
|
|
61839
62421
|
master_images_base64: imagesBase64Schema("Reference images for the master generation", "master_images"),
|
|
61840
|
-
|
|
62422
|
+
confirmToken: confirmTokenParam,
|
|
61841
62423
|
...sharedImageSchema
|
|
61842
62424
|
})
|
|
61843
62425
|
},
|
|
@@ -61846,7 +62428,16 @@ function registerSetTools(server, client2) {
|
|
|
61846
62428
|
if (args.scenes && args.count || !args.scenes && !args.count) {
|
|
61847
62429
|
throw new McpToolError("Provide exactly one of `scenes` or `count`.");
|
|
61848
62430
|
}
|
|
61849
|
-
const
|
|
62431
|
+
const { confirmToken, ...request } = args;
|
|
62432
|
+
const gate = await confirmLocalInputs(extra, {
|
|
62433
|
+
tool: "gemini_image_set",
|
|
62434
|
+
action: "image.set",
|
|
62435
|
+
description: "Send local master image input(s) to the Gemini API",
|
|
62436
|
+
endpoint: "/v1beta/models/{model}:generateContent",
|
|
62437
|
+
imagePaths: args.master_images,
|
|
62438
|
+
request,
|
|
62439
|
+
confirmToken
|
|
62440
|
+
});
|
|
61850
62441
|
if (gate) return gate;
|
|
61851
62442
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
61852
62443
|
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
@@ -61975,8 +62566,7 @@ function isValidLibraryName(name) {
|
|
|
61975
62566
|
}
|
|
61976
62567
|
function createR2Library(bucket, opts) {
|
|
61977
62568
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
61978
|
-
|
|
61979
|
-
const tenantId = () => tenantCache ??= Promise.resolve(typeof opts.tenant === "function" ? opts.tenant() : opts.tenant);
|
|
62569
|
+
const tenantId = tenantResolver(opts.tenant);
|
|
61980
62570
|
const assertName = (name) => {
|
|
61981
62571
|
if (!isValidLibraryName(name)) {
|
|
61982
62572
|
throw new Error(`Invalid library name "${name}" \u2014 use 1-40 lowercase letters, digits, "-" or "_" (e.g. "finn", "bold-cartoon-sports").`);
|
|
@@ -62583,6 +63173,17 @@ var GeminiClient = class {
|
|
|
62583
63173
|
const blob = await fileBlob(path, { type: mimeType, maxBytes: FILE_MAX_BYTES, label: "Video" });
|
|
62584
63174
|
return this.uploadToFilesApi(blob, mimeType, basename(path), blob.size);
|
|
62585
63175
|
}
|
|
63176
|
+
/**
|
|
63177
|
+
* Upload ANY local file (image, video, audio) by streaming it from disk —
|
|
63178
|
+
* the {@link uploadVideo} path without the video label. `gemini_upload_file`
|
|
63179
|
+
* `path` uses this: reading the file into a Buffer, base64-encoding it and
|
|
63180
|
+
* decoding it back held ~3x the file in memory and hit V8's max string
|
|
63181
|
+
* length on a few-hundred-MB video (chrischall/fleet-audit#117).
|
|
63182
|
+
*/
|
|
63183
|
+
async uploadFile(path, mimeType, displayName = basename(path)) {
|
|
63184
|
+
const blob = await fileBlob(path, { type: mimeType, maxBytes: FILE_MAX_BYTES, label: "File" });
|
|
63185
|
+
return this.uploadToFilesApi(blob, mimeType, displayName, blob.size);
|
|
63186
|
+
}
|
|
62586
63187
|
/**
|
|
62587
63188
|
* Upload already-in-memory bytes (a fetched `images_url`, a `data_base64`
|
|
62588
63189
|
* payload, an inline `gemini_upload_file` body) to the Files API. Same three
|
|
@@ -63208,10 +63809,9 @@ function isTransientPoll(err) {
|
|
|
63208
63809
|
function isDeliveryUnsupported(err) {
|
|
63209
63810
|
return err instanceof ApiError && err.status === 400 && /delivery/i.test(err.message);
|
|
63210
63811
|
}
|
|
63211
|
-
function hostedStorage() {
|
|
63212
|
-
const blob = blobStoreFromEnv();
|
|
63812
|
+
function hostedStorage(blob = blobStoreFromEnv()) {
|
|
63213
63813
|
if (!blob) return {};
|
|
63214
|
-
const tenant = () => tenantIdFor(readEnvVar("GEMINI_API_KEY") ?? "local");
|
|
63814
|
+
const tenant = pinnedTenant(blob.bucket, () => tenantIdFor(readEnvVar("GEMINI_API_KEY") ?? "local"));
|
|
63215
63815
|
return {
|
|
63216
63816
|
mediaSink: createR2Sink(blob.bucket, {
|
|
63217
63817
|
// The SAME `links` object the minter gets below. Media GETs and upload
|
|
@@ -63329,7 +63929,7 @@ function registerInteractTools(server, client2) {
|
|
|
63329
63929
|
server.registerTool(
|
|
63330
63930
|
"gemini_interact",
|
|
63331
63931
|
{
|
|
63332
|
-
description: "Preferred tool for iterative refinement of a single image \u2014 multi-turn generation and editing via the Interactions API. To refine, pass the returned interaction `id` as `previous_interaction_id` on the next call; do NOT start a new interaction or re-send the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. " + recoveryDescription + "Output is JPEG.",
|
|
63932
|
+
description: "Preferred tool for iterative refinement of a single image \u2014 multi-turn generation and editing via the Interactions API. To refine, pass the returned interaction `id` as `previous_interaction_id` on the next call; do NOT start a new interaction or re-send the image for each tweak. `continue_last: true` chains from this session's most recent interaction without threading the id. " + recoveryDescription + "Output is JPEG. " + confirmNote("Local file inputs are confirmed first"),
|
|
63333
63933
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
63334
63934
|
inputSchema: external_exports.object({
|
|
63335
63935
|
input: external_exports.string().min(1).describe("Text prompt or editing instruction"),
|
|
@@ -63358,7 +63958,7 @@ function registerInteractTools(server, client2) {
|
|
|
63358
63958
|
async: asyncSchema,
|
|
63359
63959
|
max_wait_ms: maxWaitMsSchema,
|
|
63360
63960
|
from_clipboard: external_exports.boolean().optional().describe("Use the image currently on the macOS system clipboard as an input (downscaled to JPEG)"),
|
|
63361
|
-
|
|
63961
|
+
confirmToken: confirmTokenParam
|
|
63362
63962
|
})
|
|
63363
63963
|
},
|
|
63364
63964
|
async (args, extra) => {
|
|
@@ -63383,7 +63983,17 @@ function registerInteractTools(server, client2) {
|
|
|
63383
63983
|
if (previousInteractionId && images?.length) {
|
|
63384
63984
|
({ kept: images, dropped } = splitReattachedOutputs(images, client2.session.writtenOutputs));
|
|
63385
63985
|
}
|
|
63386
|
-
const
|
|
63986
|
+
const { confirmToken, ...request } = args;
|
|
63987
|
+
const gate = await confirmLocalInputs(extra, {
|
|
63988
|
+
tool: "gemini_interact",
|
|
63989
|
+
action: "interaction.create",
|
|
63990
|
+
description: "Send local file input(s) to the Gemini Interactions API",
|
|
63991
|
+
endpoint: "/v1beta/interactions",
|
|
63992
|
+
imagePaths: images,
|
|
63993
|
+
videoPath: args.video_path,
|
|
63994
|
+
request: { ...request, images, previous_interaction_id: previousInteractionId },
|
|
63995
|
+
confirmToken
|
|
63996
|
+
});
|
|
63387
63997
|
if (gate) return gate;
|
|
63388
63998
|
const model = resolveModel(args.model, readEnvVar("GEMINI_IMAGE_MODEL"));
|
|
63389
63999
|
const aspectRatio = resolveAspectRatio(args, ASPECT_RATIOS);
|
|
@@ -63492,7 +64102,7 @@ function registerVideoTools(server, client2) {
|
|
|
63492
64102
|
server.registerTool(
|
|
63493
64103
|
"gemini_video_generate",
|
|
63494
64104
|
{
|
|
63495
|
-
description: 'Generate a short video (~10s) via the Gemini omni model: text\u2192video, image\u2192video / reference\u2192video (supply reference image[s]), interpolate between two stills (pass first frame then last frame as images), or continue a prior video (task: "edit" or "extend" + previous_interaction_id / continue_last; extensions add ~3-10s each, to ~40s total). Cost scales with `resolution` \u2014 draft at 360p, keep at 1080p/4k. Output is written to disk as MP4 (video has no inline MCP block). Video runs long \u2014 give it a `max_wait_ms` budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account.',
|
|
64105
|
+
description: 'Generate a short video (~10s) via the Gemini omni model: text\u2192video, image\u2192video / reference\u2192video (supply reference image[s]), interpolate between two stills (pass first frame then last frame as images), or continue a prior video (task: "edit" or "extend" + previous_interaction_id / continue_last; extensions add ~3-10s each, to ~40s total). Cost scales with `resolution` \u2014 draft at 360p, keep at 1080p/4k. Output is written to disk as MP4 (video has no inline MCP block). Video runs long \u2014 give it a `max_wait_ms` budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. ' + confirmNote("Local file inputs are confirmed first"),
|
|
63496
64106
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
63497
64107
|
inputSchema: external_exports.object({
|
|
63498
64108
|
prompt: external_exports.string().min(1).describe("Description of the video to generate (or the edit instruction when task=edit)"),
|
|
@@ -63516,7 +64126,7 @@ function registerVideoTools(server, client2) {
|
|
|
63516
64126
|
idempotency_key: idempotencyKeySchema,
|
|
63517
64127
|
async: asyncSchema,
|
|
63518
64128
|
max_wait_ms: maxWaitMsSchema,
|
|
63519
|
-
|
|
64129
|
+
confirmToken: confirmTokenParam
|
|
63520
64130
|
})
|
|
63521
64131
|
},
|
|
63522
64132
|
async (args, extra) => {
|
|
@@ -63530,7 +64140,16 @@ function registerVideoTools(server, client2) {
|
|
|
63530
64140
|
}
|
|
63531
64141
|
previousInteractionId = client2.session.lastVideoInteractionId;
|
|
63532
64142
|
}
|
|
63533
|
-
const
|
|
64143
|
+
const { confirmToken, ...request } = args;
|
|
64144
|
+
const gate = await confirmLocalInputs(extra, {
|
|
64145
|
+
tool: "gemini_video_generate",
|
|
64146
|
+
action: "video.generate",
|
|
64147
|
+
description: "Send local image input(s) to the Gemini omni API",
|
|
64148
|
+
endpoint: "/v1beta/interactions",
|
|
64149
|
+
imagePaths: args.images,
|
|
64150
|
+
request: { ...request, previous_interaction_id: previousInteractionId },
|
|
64151
|
+
confirmToken
|
|
64152
|
+
});
|
|
63534
64153
|
if (gate) return gate;
|
|
63535
64154
|
const model = args.model?.trim() || DEFAULT_VIDEO_MODEL;
|
|
63536
64155
|
const aspectRatio = resolveAspectRatio(args, VIDEO_ASPECT_RATIOS);
|
|
@@ -63595,7 +64214,7 @@ function registerMusicTools(server, client2) {
|
|
|
63595
64214
|
server.registerTool(
|
|
63596
64215
|
"gemini_music_generate",
|
|
63597
64216
|
{
|
|
63598
|
-
description: "Generate music from a text prompt (mood, genre, instruments, structure, or lyrics inline) via a Lyria model: lyria-3-clip-preview (30s instrumental clip, default, cheapest), lyria-3.5 (full-length song with vocals) or lyria-3-pro-preview (longer-form). Output is MP3, written to disk (or returned inline). Single-turn: a track cannot be refined by a follow-up call, so put the whole brief in the prompt. Runs long \u2014 give it a `max_wait_ms` budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account.",
|
|
64217
|
+
description: "Generate music from a text prompt (mood, genre, instruments, structure, or lyrics inline) via a Lyria model: lyria-3-clip-preview (30s instrumental clip, default, cheapest), lyria-3.5 (full-length song with vocals) or lyria-3-pro-preview (longer-form). Output is MP3, written to disk (or returned inline). Single-turn: a track cannot be refined by a follow-up call, so put the whole brief in the prompt. Runs long \u2014 give it a `max_wait_ms` budget (or `async: true` + gemini_get_result on a local install), or raise `timeout_ms`. Needs a funded account. " + confirmNote("Local file inputs are confirmed first"),
|
|
63599
64218
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
63600
64219
|
inputSchema: external_exports.object({
|
|
63601
64220
|
prompt: external_exports.string().min(1).describe("Description of the music: mood, genre, instruments, tempo, structure, or lyrics"),
|
|
@@ -63613,13 +64232,22 @@ function registerMusicTools(server, client2) {
|
|
|
63613
64232
|
idempotency_key: idempotencyKeySchema,
|
|
63614
64233
|
async: asyncSchema,
|
|
63615
64234
|
max_wait_ms: maxWaitMsSchema,
|
|
63616
|
-
|
|
64235
|
+
confirmToken: confirmTokenParam
|
|
63617
64236
|
})
|
|
63618
64237
|
},
|
|
63619
64238
|
async (args, extra) => {
|
|
63620
64239
|
assertLocalInputsAvailable(client2.mediaSink, args);
|
|
63621
64240
|
const model = args.model ?? DEFAULT_MUSIC_MODEL;
|
|
63622
|
-
const
|
|
64241
|
+
const { confirmToken, ...request } = args;
|
|
64242
|
+
const gate = await confirmLocalInputs(extra, {
|
|
64243
|
+
tool: "gemini_music_generate",
|
|
64244
|
+
action: "music.generate",
|
|
64245
|
+
description: "Send local image input(s) to the Gemini Lyria API",
|
|
64246
|
+
endpoint: "/v1beta/interactions",
|
|
64247
|
+
imagePaths: args.images,
|
|
64248
|
+
request,
|
|
64249
|
+
confirmToken
|
|
64250
|
+
});
|
|
63623
64251
|
if (gate) return gate;
|
|
63624
64252
|
const fingerprint = fingerprintRequest("gemini_music_generate", {
|
|
63625
64253
|
model,
|
|
@@ -63781,6 +64409,7 @@ function registerUsageTools(server, client2) {
|
|
|
63781
64409
|
// src/tools/files.ts
|
|
63782
64410
|
init_zod();
|
|
63783
64411
|
init_dist2();
|
|
64412
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
63784
64413
|
var UPLOAD_URL_MAX_BYTES_DISK = 100 * 1024 * 1024;
|
|
63785
64414
|
var UPLOAD_URL_MAX_BYTES_HOSTED = 25 * 1024 * 1024;
|
|
63786
64415
|
var UPLOAD_ACCEPT = ["image/", "video/", "audio/"];
|
|
@@ -63806,7 +64435,7 @@ function registerFileTools(server, client2) {
|
|
|
63806
64435
|
server.registerTool(
|
|
63807
64436
|
"gemini_upload_file",
|
|
63808
64437
|
{
|
|
63809
|
-
description: "Upload a file \u2014 an image, reference photo, picture, screenshot, video or audio clip \u2014 to the Gemini Files API ONCE, and get back a reusable `file_uri` (`files/<id>`) to attach to later image, video or music generations. Keywords: upload, upload file, upload image, upload photo, attach, reference image, reference photo, file_uri, files api, image reference, reuse across calls. Use this instead of pasting base64 into a tool call: the reference is a short string, so no image bytes ever enter the conversation, and it can be reused across many generations until it expires (~48h). Provide exactly one of `url` (the server downloads it), `data_base64`" + (onDisk ? ", or `path` (a local file)." : ", or `r2_key` (re-upload media this connector generated, from media[].r2_key in an earlier result).") + (onDisk ? "" : " To upload a local file without base64: mint a signed PUT URL with gemini_get_upload_url, PUT the bytes to it, then pass the returned r2_key here."),
|
|
64438
|
+
description: "Upload a file \u2014 an image, reference photo, picture, screenshot, video or audio clip \u2014 to the Gemini Files API ONCE, and get back a reusable `file_uri` (`files/<id>`) to attach to later image, video or music generations. Keywords: upload, upload file, upload image, upload photo, attach, reference image, reference photo, file_uri, files api, image reference, reuse across calls. Use this instead of pasting base64 into a tool call: the reference is a short string, so no image bytes ever enter the conversation, and it can be reused across many generations until it expires (~48h). Provide exactly one of `url` (the server downloads it), `data_base64`" + (onDisk ? ", or `path` (a local file)." : ", or `r2_key` (re-upload media this connector generated, from media[].r2_key in an earlier result).") + (onDisk ? " " + confirmNote("A local `path` is confirmed first") : " To upload a local file without base64: mint a signed PUT URL with gemini_get_upload_url, PUT the bytes to it, then pass the returned r2_key here."),
|
|
63810
64439
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
63811
64440
|
inputSchema: external_exports.object({
|
|
63812
64441
|
url: external_exports.string().url().optional().describe(
|
|
@@ -63821,7 +64450,7 @@ function registerFileTools(server, client2) {
|
|
|
63821
64450
|
),
|
|
63822
64451
|
mime_type: external_exports.string().regex(/^[\w.+-]+\/[\w.+-]+$/, "must be a MIME type like image/png").optional().describe("Override the detected MIME type (sniffed from the bytes / taken from the server response otherwise)"),
|
|
63823
64452
|
display_name: external_exports.string().min(1).optional().describe("Human-readable name recorded against the upload"),
|
|
63824
|
-
|
|
64453
|
+
confirmToken: confirmTokenParam
|
|
63825
64454
|
})
|
|
63826
64455
|
},
|
|
63827
64456
|
async (args, extra) => {
|
|
@@ -63831,6 +64460,7 @@ function registerFileTools(server, client2) {
|
|
|
63831
64460
|
sources.length === 0 ? "Provide exactly one of `url`, `data_base64`" + (onDisk ? " or `path`." : " or `r2_key`.") : `Provide exactly one source, not ${sources.length} (${sources.join(", ")}).`
|
|
63832
64461
|
);
|
|
63833
64462
|
}
|
|
64463
|
+
let localUpload;
|
|
63834
64464
|
if (args.path) {
|
|
63835
64465
|
if (!onDisk) {
|
|
63836
64466
|
throw new McpToolError(
|
|
@@ -63838,13 +64468,31 @@ function registerFileTools(server, client2) {
|
|
|
63838
64468
|
{ hint: "Use `url`, `r2_key` (gemini_get_upload_url), or `data_base64`." }
|
|
63839
64469
|
);
|
|
63840
64470
|
}
|
|
63841
|
-
const
|
|
63842
|
-
|
|
63843
|
-
|
|
63844
|
-
|
|
63845
|
-
|
|
63846
|
-
|
|
64471
|
+
const { resolveImagePath: resolveImagePath2, detectUploadMime: detectUploadMime2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
64472
|
+
const resolved = resolveImagePath2(args.path);
|
|
64473
|
+
const mimeType = args.mime_type ?? await detectUploadMime2(resolved);
|
|
64474
|
+
if (!mimeType) {
|
|
64475
|
+
throw new McpToolError(
|
|
64476
|
+
`Cannot tell what kind of file ${resolved} is from its bytes or extension \u2014 pass \`mime_type\` (e.g. video/mp4, audio/mp3, image/gif) so it is not uploaded under the wrong type.`,
|
|
64477
|
+
{ hint: "Pass mime_type explicitly." }
|
|
64478
|
+
);
|
|
64479
|
+
}
|
|
64480
|
+
const { size } = await stat3(resolved);
|
|
64481
|
+
const inputs = [{ path: resolved, mimeType, size }];
|
|
64482
|
+
const gate = await confirmWrite(extra, {
|
|
64483
|
+
tool: "gemini_upload_file",
|
|
64484
|
+
action: "file.upload",
|
|
64485
|
+
message: "Review and confirm uploading this local file to Google:",
|
|
64486
|
+
description: "Upload a local file to the Gemini Files API",
|
|
64487
|
+
method: "POST",
|
|
64488
|
+
path: "/upload/v1beta/files",
|
|
64489
|
+
body: { inputs },
|
|
64490
|
+
target: resolved,
|
|
64491
|
+
payload: { inputs, display_name: args.display_name },
|
|
64492
|
+
confirmToken: args.confirmToken
|
|
64493
|
+
});
|
|
63847
64494
|
if (gate) return gate;
|
|
64495
|
+
localUpload = { path: resolved, mimeType };
|
|
63848
64496
|
}
|
|
63849
64497
|
const uploaded = await withProgressHeartbeat(extra, "Uploading to the Gemini Files API", async () => {
|
|
63850
64498
|
if (args.url) {
|
|
@@ -63873,17 +64521,14 @@ function registerFileTools(server, client2) {
|
|
|
63873
64521
|
if (args.data_base64) {
|
|
63874
64522
|
const { decodeImageInput: decodeImageInput2 } = await Promise.resolve().then(() => (init_images(), images_exports));
|
|
63875
64523
|
const decoded = decodeImageInput2(args.data_base64);
|
|
63876
|
-
const
|
|
63877
|
-
return client2.uploadBytes(
|
|
63878
|
-
}
|
|
63879
|
-
const
|
|
63880
|
-
|
|
63881
|
-
|
|
63882
|
-
|
|
63883
|
-
|
|
63884
|
-
bytes,
|
|
63885
|
-
args.mime_type ?? inline.mimeType,
|
|
63886
|
-
args.display_name ?? (resolved.split(/[\\/]/).pop() || "upload")
|
|
64524
|
+
const bytes = base64ToBytes(decoded.base64);
|
|
64525
|
+
return client2.uploadBytes(bytes, args.mime_type ?? decoded.mimeType, args.display_name ?? "upload");
|
|
64526
|
+
}
|
|
64527
|
+
const local = localUpload;
|
|
64528
|
+
return client2.uploadFile(
|
|
64529
|
+
local.path,
|
|
64530
|
+
local.mimeType,
|
|
64531
|
+
args.display_name ?? (local.path.split(/[\\/]/).pop() || "upload")
|
|
63887
64532
|
);
|
|
63888
64533
|
});
|
|
63889
64534
|
return uploadResult(uploaded);
|
|
@@ -64017,15 +64662,25 @@ function registerFileTools(server, client2) {
|
|
|
64017
64662
|
server.registerTool(
|
|
64018
64663
|
"gemini_delete_file",
|
|
64019
64664
|
{
|
|
64020
|
-
description: "Delete an uploaded file, image or photo (by file_uri) from the Gemini Files API before its ~48h expiry. Any tool call still referencing it will then fail with a generic 404, so delete only references you are finished with.",
|
|
64665
|
+
description: "Delete an uploaded file, image or photo (by file_uri) from the Gemini Files API before its ~48h expiry. Any tool call still referencing it will then fail with a generic 404, so delete only references you are finished with. " + confirmNote(),
|
|
64021
64666
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
64022
64667
|
inputSchema: external_exports.object({
|
|
64023
64668
|
file_uri: external_exports.string().min(1).describe("The `files/<id>` reference (or full uri) to delete"),
|
|
64024
|
-
|
|
64669
|
+
confirmToken: confirmTokenParam
|
|
64025
64670
|
})
|
|
64026
64671
|
},
|
|
64027
|
-
async (args) => {
|
|
64028
|
-
const gate =
|
|
64672
|
+
async (args, extra) => {
|
|
64673
|
+
const gate = await confirmWrite(extra, {
|
|
64674
|
+
tool: "gemini_delete_file",
|
|
64675
|
+
action: "file.delete",
|
|
64676
|
+
message: "Review and confirm this deletion:",
|
|
64677
|
+
description: "Delete an uploaded Gemini file",
|
|
64678
|
+
method: "DELETE",
|
|
64679
|
+
path: `/v1beta/${args.file_uri}`,
|
|
64680
|
+
target: args.file_uri,
|
|
64681
|
+
payload: { file_uri: args.file_uri },
|
|
64682
|
+
confirmToken: args.confirmToken
|
|
64683
|
+
});
|
|
64029
64684
|
if (gate) return gate;
|
|
64030
64685
|
await client2.deleteFile(args.file_uri);
|
|
64031
64686
|
return minifiedResult({ deleted: args.file_uri });
|
|
@@ -64161,12 +64816,22 @@ function registerLibraryTools(server, client2) {
|
|
|
64161
64816
|
server.registerTool(
|
|
64162
64817
|
"gemini_delete_character",
|
|
64163
64818
|
{
|
|
64164
|
-
description: "Delete a saved character from this account's reference library. Generation calls naming it will then fail until it is re-saved.",
|
|
64819
|
+
description: "Delete a saved character from this account's reference library. Generation calls naming it will then fail until it is re-saved. " + confirmNote(),
|
|
64165
64820
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
64166
|
-
inputSchema: external_exports.object({ name: NAME_FIELD,
|
|
64821
|
+
inputSchema: external_exports.object({ name: NAME_FIELD, confirmToken: confirmTokenParam })
|
|
64167
64822
|
},
|
|
64168
|
-
async (args) => {
|
|
64169
|
-
const gate =
|
|
64823
|
+
async (args, extra) => {
|
|
64824
|
+
const gate = await confirmWrite(extra, {
|
|
64825
|
+
tool: "gemini_delete_character",
|
|
64826
|
+
action: "character.delete",
|
|
64827
|
+
message: "Review and confirm this deletion:",
|
|
64828
|
+
description: "Delete a saved character (its reference image and description)",
|
|
64829
|
+
method: "DELETE",
|
|
64830
|
+
path: `characters/${args.name}`,
|
|
64831
|
+
target: args.name,
|
|
64832
|
+
payload: { name: args.name },
|
|
64833
|
+
confirmToken: args.confirmToken
|
|
64834
|
+
});
|
|
64170
64835
|
if (gate) return gate;
|
|
64171
64836
|
const existed = await library.deleteCharacter(args.name);
|
|
64172
64837
|
if (!existed) {
|
|
@@ -64230,12 +64895,22 @@ function registerLibraryTools(server, client2) {
|
|
|
64230
64895
|
server.registerTool(
|
|
64231
64896
|
"gemini_delete_style",
|
|
64232
64897
|
{
|
|
64233
|
-
description: "Delete a saved style preset from this account's reference library.",
|
|
64898
|
+
description: "Delete a saved style preset from this account's reference library. " + confirmNote(),
|
|
64234
64899
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
64235
|
-
inputSchema: external_exports.object({ name: NAME_FIELD,
|
|
64900
|
+
inputSchema: external_exports.object({ name: NAME_FIELD, confirmToken: confirmTokenParam })
|
|
64236
64901
|
},
|
|
64237
|
-
async (args) => {
|
|
64238
|
-
const gate =
|
|
64902
|
+
async (args, extra) => {
|
|
64903
|
+
const gate = await confirmWrite(extra, {
|
|
64904
|
+
tool: "gemini_delete_style",
|
|
64905
|
+
action: "style.delete",
|
|
64906
|
+
message: "Review and confirm this deletion:",
|
|
64907
|
+
description: "Delete a saved style preset",
|
|
64908
|
+
method: "DELETE",
|
|
64909
|
+
path: `styles/${args.name}`,
|
|
64910
|
+
target: args.name,
|
|
64911
|
+
payload: { name: args.name },
|
|
64912
|
+
confirmToken: args.confirmToken
|
|
64913
|
+
});
|
|
64239
64914
|
if (gate) return gate;
|
|
64240
64915
|
const existed = await library.deleteStyle(args.name);
|
|
64241
64916
|
if (!existed) {
|