@a1-x-tech/mcp-google-slides 0.1.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/LICENSE +21 -0
- package/README.md +308 -0
- package/README.ru.md +308 -0
- package/dist/client.js +1032 -0
- package/dist/config.js +87 -0
- package/dist/index.js +131 -0
- package/dist/telemetry.js +120 -0
- package/dist/tools/comments.js +39 -0
- package/dist/tools/elements.js +164 -0
- package/dist/tools/export.js +54 -0
- package/dist/tools/notes.js +37 -0
- package/dist/tools/presentations.js +87 -0
- package/dist/tools/raw.js +30 -0
- package/dist/tools/slides.js +117 -0
- package/dist/tools/tables.js +87 -0
- package/dist/tools/text.js +129 -0
- package/dist/tools/util.js +73 -0
- package/dist/types.js +47 -0
- package/package.json +62 -0
- package/test/dist-smoke.test.js +189 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
|
|
8
|
+
import { GoogleSlidesClient } from "../dist/client.js";
|
|
9
|
+
import { registerPresentationTools } from "../dist/tools/presentations.js";
|
|
10
|
+
import { registerSlideTools } from "../dist/tools/slides.js";
|
|
11
|
+
import { registerTextTools } from "../dist/tools/text.js";
|
|
12
|
+
import { registerElementTools } from "../dist/tools/elements.js";
|
|
13
|
+
import { registerTableTools } from "../dist/tools/tables.js";
|
|
14
|
+
import { registerNoteTools } from "../dist/tools/notes.js";
|
|
15
|
+
import { registerCommentTools } from "../dist/tools/comments.js";
|
|
16
|
+
import { registerExportTools } from "../dist/tools/export.js";
|
|
17
|
+
import { registerRawTool } from "../dist/tools/raw.js";
|
|
18
|
+
|
|
19
|
+
const ALL_TOOLS = [
|
|
20
|
+
"add_slide",
|
|
21
|
+
"batch_update",
|
|
22
|
+
"create_image",
|
|
23
|
+
"create_presentation",
|
|
24
|
+
"create_shape",
|
|
25
|
+
"create_table",
|
|
26
|
+
"delete_object",
|
|
27
|
+
"duplicate_slide",
|
|
28
|
+
"edit_table",
|
|
29
|
+
"export_presentation",
|
|
30
|
+
"get_page",
|
|
31
|
+
"get_presentation",
|
|
32
|
+
"get_speaker_notes",
|
|
33
|
+
"get_thumbnail",
|
|
34
|
+
"insert_text",
|
|
35
|
+
"list_layouts",
|
|
36
|
+
"list_slides",
|
|
37
|
+
"manage_comments",
|
|
38
|
+
"move_slides",
|
|
39
|
+
"raw_request",
|
|
40
|
+
"replace_image",
|
|
41
|
+
"replace_text",
|
|
42
|
+
"set_speaker_notes",
|
|
43
|
+
"set_text",
|
|
44
|
+
"update_text_style",
|
|
45
|
+
"update_transform",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
test("dist client rejects foreign-origin paths before sending the Bearer token", async () => {
|
|
49
|
+
const original = globalThis.fetch;
|
|
50
|
+
let called = false;
|
|
51
|
+
globalThis.fetch = async () => {
|
|
52
|
+
called = true;
|
|
53
|
+
return new Response("{}", { status: 200 });
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const client = new GoogleSlidesClient({
|
|
57
|
+
accessToken: "SECRET",
|
|
58
|
+
apiBase: "https://slides.googleapis.com",
|
|
59
|
+
timeoutMs: 1000,
|
|
60
|
+
maxRetries: 0,
|
|
61
|
+
});
|
|
62
|
+
await assert.rejects(() => client.request("GET", "https://example.invalid/steal"), /foreign origin/);
|
|
63
|
+
assert.equal(called, false);
|
|
64
|
+
} finally {
|
|
65
|
+
globalThis.fetch = original;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("dist client sends the Bearer token and JSON bodies", async () => {
|
|
70
|
+
const original = globalThis.fetch;
|
|
71
|
+
let seen;
|
|
72
|
+
globalThis.fetch = async (url, init) => {
|
|
73
|
+
seen = { url: String(url), auth: init.headers.Authorization, body: JSON.parse(init.body) };
|
|
74
|
+
return new Response('{"presentationId":"p-1"}', { status: 200 });
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
const client = new GoogleSlidesClient({
|
|
78
|
+
accessToken: "SECRET",
|
|
79
|
+
apiBase: "https://slides.googleapis.com",
|
|
80
|
+
timeoutMs: 1000,
|
|
81
|
+
maxRetries: 0,
|
|
82
|
+
});
|
|
83
|
+
await client.createPresentation("Smoke");
|
|
84
|
+
assert.equal(seen.url, "https://slides.googleapis.com/v1/presentations");
|
|
85
|
+
assert.equal(seen.auth, "Bearer SECRET");
|
|
86
|
+
assert.deepEqual(seen.body, { title: "Smoke" });
|
|
87
|
+
} finally {
|
|
88
|
+
globalThis.fetch = original;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("dist registers the expected tools", () => {
|
|
93
|
+
const names = [];
|
|
94
|
+
const server = {
|
|
95
|
+
registerTool(name) {
|
|
96
|
+
names.push(name);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
const client = {};
|
|
100
|
+
|
|
101
|
+
registerPresentationTools(server, client);
|
|
102
|
+
registerSlideTools(server, client);
|
|
103
|
+
registerTextTools(server, client);
|
|
104
|
+
registerElementTools(server, client);
|
|
105
|
+
registerTableTools(server, client);
|
|
106
|
+
registerNoteTools(server, client);
|
|
107
|
+
registerCommentTools(server, client);
|
|
108
|
+
registerExportTools(server, client);
|
|
109
|
+
registerRawTool(server, client);
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(names.sort(), ALL_TOOLS);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("dist binary completes a real MCP handshake over stdio and lists every tool", async () => {
|
|
115
|
+
const transport = new StdioClientTransport({
|
|
116
|
+
command: process.execPath,
|
|
117
|
+
args: [fileURLToPath(new URL("../dist/index.js", import.meta.url))],
|
|
118
|
+
env: {
|
|
119
|
+
...process.env,
|
|
120
|
+
GOOGLE_SLIDES_ACCESS_TOKEN: "test-token",
|
|
121
|
+
ASKADS_TELEMETRY: "0", // keep the suite offline
|
|
122
|
+
},
|
|
123
|
+
stderr: "pipe",
|
|
124
|
+
});
|
|
125
|
+
const client = new Client({ name: "dist-smoke", version: "0.0.0" });
|
|
126
|
+
await client.connect(transport);
|
|
127
|
+
try {
|
|
128
|
+
const server = client.getServerVersion();
|
|
129
|
+
assert.equal(server?.name, "mcp-google-slides");
|
|
130
|
+
assert.match(String(server?.version), /^\d+\.\d+\.\d+$/);
|
|
131
|
+
|
|
132
|
+
// The instructions the calling model reads before it picks any tool.
|
|
133
|
+
const instructions = client.getInstructions();
|
|
134
|
+
assert.equal(typeof instructions, "string");
|
|
135
|
+
assert.ok(instructions.trim().length > 0, "initialize result carries no instructions");
|
|
136
|
+
assert.match(instructions, /Google Slides API v1/);
|
|
137
|
+
|
|
138
|
+
const { tools } = await client.listTools();
|
|
139
|
+
assert.deepEqual(tools.map((t) => t.name).sort(), ALL_TOOLS);
|
|
140
|
+
|
|
141
|
+
const listSlides = tools.find((t) => t.name === "list_slides");
|
|
142
|
+
assert.equal(listSlides.annotations?.readOnlyHint, true);
|
|
143
|
+
assert.ok(listSlides.inputSchema?.properties?.presentation_id, "input schema must reach the client");
|
|
144
|
+
} finally {
|
|
145
|
+
await client.close();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The degraded-start contract: without any credentials the binary must still
|
|
151
|
+
* start, list every tool, open the instructions with the fix, and answer a
|
|
152
|
+
* tool call with the actionable error — offline: the CredentialsError fires
|
|
153
|
+
* before any fetch, so this test never touches the network.
|
|
154
|
+
*/
|
|
155
|
+
test("dist binary starts without credentials: handshake, tool list, actionable call error", async () => {
|
|
156
|
+
const env = Object.fromEntries(
|
|
157
|
+
Object.entries(process.env).filter(
|
|
158
|
+
([key, value]) => value !== undefined && !key.startsWith("GOOGLE_SLIDES_"),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
env.ASKADS_TELEMETRY = "0"; // keep the suite offline
|
|
162
|
+
const transport = new StdioClientTransport({
|
|
163
|
+
command: process.execPath,
|
|
164
|
+
args: [fileURLToPath(new URL("../dist/index.js", import.meta.url))],
|
|
165
|
+
env,
|
|
166
|
+
stderr: "pipe",
|
|
167
|
+
});
|
|
168
|
+
const client = new Client({ name: "dist-smoke-unconfigured", version: "0.0.0" });
|
|
169
|
+
await client.connect(transport);
|
|
170
|
+
try {
|
|
171
|
+
// The model must read the fix before it picks a tool.
|
|
172
|
+
const instructions = client.getInstructions() ?? "";
|
|
173
|
+
assert.match(instructions, /not connected/);
|
|
174
|
+
assert.match(instructions, /GOOGLE_SLIDES_CLIENT_ID/);
|
|
175
|
+
assert.match(instructions, /restart/);
|
|
176
|
+
|
|
177
|
+
const { tools } = await client.listTools();
|
|
178
|
+
assert.deepEqual(tools.map((t) => t.name).sort(), ALL_TOOLS);
|
|
179
|
+
|
|
180
|
+
// A tool call fails with the exact message instead of killing the server.
|
|
181
|
+
const result = await client.callTool({ name: "list_slides", arguments: { presentation_id: "smoke-deck" } });
|
|
182
|
+
assert.equal(result.isError, true);
|
|
183
|
+
const text = result.content.map((c) => c.text ?? "").join(" ");
|
|
184
|
+
assert.match(text, /Google OAuth credentials are required: set GOOGLE_SLIDES_CLIENT_ID/);
|
|
185
|
+
assert.match(text, /restart the server/);
|
|
186
|
+
} finally {
|
|
187
|
+
await client.close();
|
|
188
|
+
}
|
|
189
|
+
});
|