mcp-dashboards 2.0.0 → 2.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.
@@ -0,0 +1,8 @@
1
+ import http from "node:http";
2
+ export interface PreviewUrls {
3
+ httpUrl: string;
4
+ fileUrl: string;
5
+ }
6
+ export declare function getPreviewUrls(data: any): Promise<PreviewUrls | null>;
7
+ export declare function getPreviewServer(): http.Server | null;
8
+ //# sourceMappingURL=preview-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-server.d.ts","sourceRoot":"","sources":["../preview-server.ts"],"names":[],"mappings":"AAEA,OAAO,IAAI,MAAM,WAAW,CAAC;AAyG7B,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAiB3E;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAErD"}
@@ -0,0 +1,115 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import http from "node:http";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const DIST_DIR = __filename.endsWith(".ts") ? path.join(__dirname, "dist") : __dirname;
10
+ const MAX_CHARTS = 50;
11
+ const TEMP_DIR = path.join(os.tmpdir(), "mcp-dashboards");
12
+ const chartStore = new Map();
13
+ let cachedHtml = null;
14
+ let httpServer = null;
15
+ let serverPort = null;
16
+ async function loadHtml() {
17
+ if (cachedHtml)
18
+ return cachedHtml;
19
+ const htmlPath = path.join(DIST_DIR, "mcp-app.html");
20
+ cachedHtml = await fs.readFile(htmlPath, "utf-8");
21
+ return cachedHtml;
22
+ }
23
+ function injectChartData(html, data) {
24
+ const payload = JSON.stringify(data).replace(/<\//g, "<\\/");
25
+ return html.replace("</head>", `<script>window.__CHART_DATA__=${payload};</script></head>`);
26
+ }
27
+ function storeChart(data) {
28
+ const id = crypto.randomBytes(6).toString("hex");
29
+ chartStore.set(id, data);
30
+ // Evict oldest if over limit
31
+ if (chartStore.size > MAX_CHARTS) {
32
+ const firstKey = chartStore.keys().next().value;
33
+ if (firstKey)
34
+ chartStore.delete(firstKey);
35
+ }
36
+ return id;
37
+ }
38
+ async function ensurePreviewServer() {
39
+ if (serverPort !== null)
40
+ return serverPort;
41
+ return new Promise((resolve, reject) => {
42
+ const server = http.createServer(async (req, res) => {
43
+ try {
44
+ if (!req.url) {
45
+ res.writeHead(404);
46
+ res.end("Not Found");
47
+ return;
48
+ }
49
+ const match = req.url.match(/^\/chart\/([a-f0-9]+)/);
50
+ if (!match) {
51
+ res.writeHead(404);
52
+ res.end("Not Found");
53
+ return;
54
+ }
55
+ const data = chartStore.get(match[1]);
56
+ if (!data) {
57
+ res.writeHead(404, { "Content-Type": "text/html" });
58
+ res.end("<h1>Chart not found</h1><p>It may have been evicted or never existed.</p>");
59
+ return;
60
+ }
61
+ const html = await loadHtml();
62
+ const injected = injectChartData(html, data);
63
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
64
+ res.end(injected);
65
+ }
66
+ catch (err) {
67
+ res.writeHead(500);
68
+ res.end(`Server error: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ });
71
+ server.listen(0, "127.0.0.1", () => {
72
+ const addr = server.address();
73
+ if (typeof addr === "object" && addr) {
74
+ serverPort = addr.port;
75
+ httpServer = server;
76
+ process.stderr.write(`[mcp-dashboards] Preview server ready at http://localhost:${serverPort}\n`);
77
+ process.on("exit", () => server.close());
78
+ resolve(serverPort);
79
+ }
80
+ else {
81
+ reject(new Error("Failed to bind preview server"));
82
+ }
83
+ });
84
+ server.on("error", reject);
85
+ });
86
+ }
87
+ async function writeTempHtml(id, data) {
88
+ await fs.mkdir(TEMP_DIR, { recursive: true });
89
+ const filePath = path.join(TEMP_DIR, `chart-${id}.html`);
90
+ const html = await loadHtml();
91
+ const injected = injectChartData(html, data);
92
+ await fs.writeFile(filePath, injected, "utf-8");
93
+ return pathToFileURL(filePath).href;
94
+ }
95
+ export async function getPreviewUrls(data) {
96
+ if (process.env.MCP_DASHBOARDS_DISABLE_PREVIEW === "1")
97
+ return null;
98
+ try {
99
+ const id = storeChart(data);
100
+ const port = await ensurePreviewServer();
101
+ const fileUrl = await writeTempHtml(id, data);
102
+ return {
103
+ httpUrl: `http://localhost:${port}/chart/${id}`,
104
+ fileUrl,
105
+ };
106
+ }
107
+ catch (err) {
108
+ process.stderr.write(`[mcp-dashboards] Preview unavailable: ${err instanceof Error ? err.message : String(err)}\n`);
109
+ return null;
110
+ }
111
+ }
112
+ export function getPreviewServer() {
113
+ return httpServer;
114
+ }
115
+ //# sourceMappingURL=preview-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-server.js","sourceRoot":"","sources":["../preview-server.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoNpE;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAm5CxC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoPpE;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAs2CxC"}
package/dist/server.js CHANGED
@@ -1,10 +1,11 @@
1
- import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
1
+ import { getUiCapability, registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { z } from "zod";
8
+ import { getPreviewUrls } from "./preview-server.js";
8
9
  // Works both from source (server.ts) and compiled (dist/server.js)
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
@@ -154,6 +155,7 @@ function _registerChartTool(server, name, meta, inputSchema, buildResult, summar
154
155
  effects: EffectsParam,
155
156
  },
156
157
  _meta: { ui: { resourceUri: RESOURCE_URI } },
158
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
157
159
  }, async (args) => {
158
160
  const chartData = {
159
161
  ...buildResult(args),
@@ -162,15 +164,39 @@ function _registerChartTool(server, name, meta, inputSchema, buildResult, summar
162
164
  typography: args.typography,
163
165
  effects: args.effects,
164
166
  };
165
- return {
166
- content: [
167
- { type: "text", text: summarize(args) },
168
- { type: "text", text: JSON.stringify(chartData) },
169
- ],
170
- structuredContent: chartData,
171
- };
167
+ return await _buildChartResult(server, chartData, summarize(args));
172
168
  });
173
169
  }
170
+ // Tracks whether each server's connected client supports MCP Apps inline rendering.
171
+ // Set via oninitialized callback in createServer.
172
+ const _clientSupportsInline = new WeakMap();
173
+ // Builds the standard tool response. If the client doesn't support MCP Apps
174
+ // inline rendering, appends browser preview links (localhost HTTP + standalone file)
175
+ // to the text content. MCP Apps clients only get the structuredContent - they
176
+ // already render the chart inline, so preview URLs would be redundant clutter.
177
+ async function _buildChartResult(server, chartData, summary) {
178
+ const content = [
179
+ { type: "text", text: summary },
180
+ ];
181
+ if (!_clientSupportsInline.get(server)) {
182
+ const urls = await getPreviewUrls(chartData);
183
+ if (urls) {
184
+ content.push({
185
+ type: "text",
186
+ text: `\n## View this chart\n` +
187
+ `Your AI client doesn't render MCP Apps inline, so use one of these links to see the interactive chart in your browser:\n\n` +
188
+ `**Click to open (recommended):** ${urls.httpUrl}\n` +
189
+ ` - Opens instantly in your default browser\n` +
190
+ ` - Only works while this MCP server is running\n\n` +
191
+ `**Save or share:** ${urls.fileUrl}\n` +
192
+ ` - Self-contained HTML file on your disk\n` +
193
+ ` - Works offline, survives server restart, can be emailed or archived\n`,
194
+ });
195
+ }
196
+ }
197
+ content.push({ type: "text", text: JSON.stringify(chartData) });
198
+ return { content, structuredContent: chartData };
199
+ }
174
200
  /**
175
201
  * Creates a new MCP Dashboards server instance.
176
202
  */
@@ -179,6 +205,12 @@ export function createServer() {
179
205
  name: "MCP Dashboards",
180
206
  version: "2.0.0",
181
207
  });
208
+ // After initialize handshake, detect if client supports MCP Apps inline rendering.
209
+ // Clients that do will see the interactive chart; those that don't get preview URLs.
210
+ server.server.oninitialized = () => {
211
+ const caps = server.server.getClientCapabilities?.();
212
+ _clientSupportsInline.set(server, !!getUiCapability(caps));
213
+ };
182
214
  // -- Shared HTML resource --
183
215
  registerAppResource(server, RESOURCE_URI, RESOURCE_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => {
184
216
  const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
@@ -200,6 +232,7 @@ export function createServer() {
200
232
  effects: EffectsParam,
201
233
  },
202
234
  _meta: { ui: { resourceUri: RESOURCE_URI } },
235
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
203
236
  }, async (args) => {
204
237
  const chartData = {
205
238
  type: "pie",
@@ -215,13 +248,7 @@ export function createServer() {
215
248
  const summary = args.data
216
249
  .map((d) => `${d.label}: ${d.value} (${total > 0 ? ((d.value / total) * 100).toFixed(1) : "0.0"}%)`)
217
250
  .join(", ");
218
- return {
219
- content: [
220
- { type: "text", text: `${args.title}: ${summary}` },
221
- { type: "text", text: JSON.stringify(chartData) },
222
- ],
223
- structuredContent: chartData,
224
- };
251
+ return await _buildChartResult(server, chartData, `${args.title}: ${summary}`);
225
252
  });
226
253
  // -- Tool: render_bar_chart --
227
254
  registerAppTool(server, "render_bar_chart", {
@@ -238,6 +265,7 @@ export function createServer() {
238
265
  effects: EffectsParam,
239
266
  },
240
267
  _meta: { ui: { resourceUri: RESOURCE_URI } },
268
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
241
269
  }, async (args) => {
242
270
  const chartData = {
243
271
  type: "bar",
@@ -253,13 +281,7 @@ export function createServer() {
253
281
  const summary = args.datasets
254
282
  .map((ds) => `${ds.label}: [${ds.data.join(", ")}]`)
255
283
  .join("; ");
256
- return {
257
- content: [
258
- { type: "text", text: `${args.title} - ${summary}` },
259
- { type: "text", text: JSON.stringify(chartData) },
260
- ],
261
- structuredContent: chartData,
262
- };
284
+ return await _buildChartResult(server, chartData, `${args.title} - ${summary}`);
263
285
  });
264
286
  // -- Tool: render_line_chart --
265
287
  registerAppTool(server, "render_line_chart", {
@@ -276,6 +298,7 @@ export function createServer() {
276
298
  effects: EffectsParam,
277
299
  },
278
300
  _meta: { ui: { resourceUri: RESOURCE_URI } },
301
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
279
302
  }, async (args) => {
280
303
  const chartData = {
281
304
  type: "line",
@@ -291,13 +314,7 @@ export function createServer() {
291
314
  const summary = args.datasets
292
315
  .map((ds) => `${ds.label}: [${ds.data.join(", ")}]`)
293
316
  .join("; ");
294
- return {
295
- content: [
296
- { type: "text", text: `${args.title} - ${summary}` },
297
- { type: "text", text: JSON.stringify(chartData) },
298
- ],
299
- structuredContent: chartData,
300
- };
317
+ return await _buildChartResult(server, chartData, `${args.title} - ${summary}`);
301
318
  });
302
319
  // -- Tool: render_hero_metric --
303
320
  const GemTypeEnum = z.enum([
@@ -397,6 +414,7 @@ export function createServer() {
397
414
  effects: EffectsParam,
398
415
  },
399
416
  _meta: { ui: { resourceUri: RESOURCE_URI } },
417
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
400
418
  }, async (args) => {
401
419
  const chartData = {
402
420
  type: "hero_metric",
@@ -404,13 +422,7 @@ export function createServer() {
404
422
  };
405
423
  const variant = args.variant || "big_number";
406
424
  const summary = `${args.title}: [${variant}] ${args.value ?? ""}${args.unit ? " " + args.unit : ""}`;
407
- return {
408
- content: [
409
- { type: "text", text: summary },
410
- { type: "text", text: JSON.stringify(chartData) },
411
- ],
412
- structuredContent: chartData,
413
- };
425
+ return await _buildChartResult(server, chartData, summary);
414
426
  });
415
427
  // -- Tool: render_dashboard --
416
428
  const HeroSchema = z.object({
@@ -457,6 +469,7 @@ export function createServer() {
457
469
  layout: z.enum(["default", "hero-center", "kpi-top"]).optional().describe("Layout variant: default (hero above KPIs), hero-center (hero prominent), kpi-top (KPIs first)"),
458
470
  },
459
471
  _meta: { ui: { resourceUri: RESOURCE_URI } },
472
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
460
473
  }, async (args) => {
461
474
  const chartData = {
462
475
  type: "dashboard",
@@ -483,13 +496,7 @@ export function createServer() {
483
496
  const heroCount = Array.isArray(args.hero) ? args.hero.length : 1;
484
497
  parts.push(`Hero: ${heroCount} widget${heroCount > 1 ? "s" : ""}`);
485
498
  }
486
- return {
487
- content: [
488
- { type: "text", text: parts.join(" | ") },
489
- { type: "text", text: JSON.stringify(chartData) },
490
- ],
491
- structuredContent: chartData,
492
- };
499
+ return await _buildChartResult(server, chartData, parts.join(" | "));
493
500
  });
494
501
  // -- Tool: render_chart_catalog --
495
502
  registerAppTool(server, "render_chart_catalog", {
@@ -499,6 +506,7 @@ export function createServer() {
499
506
  theme: ThemeParam,
500
507
  },
501
508
  _meta: { ui: { resourceUri: RESOURCE_URI } },
509
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
502
510
  }, async (args) => {
503
511
  // CSS-delegated charts spread c.data into the payload, so array data
504
512
  // must be wrapped as { data: [...] } to land on payload.data correctly.
@@ -595,20 +603,15 @@ export function createServer() {
595
603
  theme: args.theme,
596
604
  footer: { text: "mcp-dashboards", lastUpdated: "Also available: render_table, render_from_json, render_from_url, render_live_chart, poll_http" },
597
605
  };
598
- return {
599
- content: [
600
- { type: "text", text: "Chart Catalog: 22 visual previews of every embeddable chart type. Click any card to ask about it. Standalone-only tools (table, live, auto, URL) listed in footer." },
601
- { type: "text", text: JSON.stringify(chartData) },
602
- ],
603
- structuredContent: chartData,
604
- };
606
+ return await _buildChartResult(server, chartData, "Chart Catalog: 22 visual previews of every embeddable chart type. Click any card to ask about it. Standalone-only tools (table, live, auto, URL) listed in footer.");
605
607
  });
606
608
  // -- Tool: render_theme_catalog --
607
609
  registerAppTool(server, "render_theme_catalog", {
608
610
  title: "Theme Catalog",
609
- description: "Show a visual catalog of all 20 available themes. Each card previews the theme's colors, typography, and effects. Click any card to use that theme.",
611
+ description: "Show a visual catalog of all 21 available themes. Each card previews the theme's colors, typography, and effects. Click any card to use that theme.",
610
612
  inputSchema: {},
611
613
  _meta: { ui: { resourceUri: RESOURCE_URI } },
614
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
612
615
  }, async () => {
613
616
  return {
614
617
  content: [
@@ -789,6 +792,7 @@ export function createServer() {
789
792
  effects: EffectsParam,
790
793
  },
791
794
  _meta: { ui: { resourceUri: RESOURCE_URI } },
795
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
792
796
  }, async (args) => {
793
797
  const chartData = {
794
798
  type: "scatter",
@@ -803,13 +807,7 @@ export function createServer() {
803
807
  const summary = args.datasets
804
808
  .map((ds) => `${ds.label}: ${ds.data.length} points`)
805
809
  .join("; ");
806
- return {
807
- content: [
808
- { type: "text", text: `${args.title} - ${summary}` },
809
- { type: "text", text: JSON.stringify(chartData) },
810
- ],
811
- structuredContent: chartData,
812
- };
810
+ return await _buildChartResult(server, chartData, `${args.title} - ${summary}`);
813
811
  });
814
812
  // -- Tool: render_candlestick_chart --
815
813
  registerAppTool(server, "render_candlestick_chart", {
@@ -825,6 +823,7 @@ export function createServer() {
825
823
  effects: EffectsParam,
826
824
  },
827
825
  _meta: { ui: { resourceUri: RESOURCE_URI } },
826
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
828
827
  }, async (args) => {
829
828
  const chartData = {
830
829
  type: "candlestick",
@@ -839,13 +838,7 @@ export function createServer() {
839
838
  const first = args.data[0];
840
839
  const last = args.data[args.data.length - 1];
841
840
  const change = last ? ((last.c - first.o) / first.o * 100).toFixed(2) : "0";
842
- return {
843
- content: [
844
- { type: "text", text: `${args.title}: ${args.data.length} bars, ${first?.date ?? "?"} to ${last?.date ?? "?"}, change: ${change}%` },
845
- { type: "text", text: JSON.stringify(chartData) },
846
- ],
847
- structuredContent: chartData,
848
- };
841
+ return await _buildChartResult(server, chartData, `${args.title}: ${args.data.length} bars, ${first?.date ?? "?"} to ${last?.date ?? "?"}, change: ${change}%`);
849
842
  });
850
843
  // -- Tool: render_table --
851
844
  registerAppTool(server, "render_table", {
@@ -865,6 +858,7 @@ export function createServer() {
865
858
  effects: EffectsParam,
866
859
  },
867
860
  _meta: { ui: { resourceUri: RESOURCE_URI } },
861
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
868
862
  }, async (args) => {
869
863
  const chartData = {
870
864
  type: "table",
@@ -877,13 +871,7 @@ export function createServer() {
877
871
  typography: args.typography,
878
872
  effects: args.effects,
879
873
  };
880
- return {
881
- content: [
882
- { type: "text", text: `${args.title}: ${args.rows.length} rows, ${args.columns.length} columns` },
883
- { type: "text", text: JSON.stringify(chartData) },
884
- ],
885
- structuredContent: chartData,
886
- };
874
+ return await _buildChartResult(server, chartData, `${args.title}: ${args.rows.length} rows, ${args.columns.length} columns`);
887
875
  });
888
876
  // -- Tool: render_from_json --
889
877
  registerAppTool(server, "render_from_json", {
@@ -897,6 +885,7 @@ export function createServer() {
897
885
  }).optional(),
898
886
  },
899
887
  _meta: { ui: { resourceUri: RESOURCE_URI } },
888
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
900
889
  }, async (args) => {
901
890
  const chartData = {
902
891
  type: "auto",
@@ -904,13 +893,7 @@ export function createServer() {
904
893
  data: args.data,
905
894
  options: args.options ?? {},
906
895
  };
907
- return {
908
- content: [
909
- { type: "text", text: `Auto-visualizing: ${args.title}` },
910
- { type: "text", text: JSON.stringify(chartData) },
911
- ],
912
- structuredContent: chartData,
913
- };
896
+ return await _buildChartResult(server, chartData, `Auto-visualizing: ${args.title}`);
914
897
  });
915
898
  // -- Tool: render_from_url --
916
899
  registerAppTool(server, "render_from_url", {
@@ -924,6 +907,7 @@ export function createServer() {
924
907
  }).optional(),
925
908
  },
926
909
  _meta: { ui: { resourceUri: RESOURCE_URI } },
910
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
927
911
  }, async (args) => {
928
912
  try {
929
913
  const response = await fetch(args.url, {
@@ -943,13 +927,7 @@ export function createServer() {
943
927
  data,
944
928
  options: args.options ?? {},
945
929
  };
946
- return {
947
- content: [
948
- { type: "text", text: `Fetched and visualizing: ${args.title} (from ${args.url})` },
949
- { type: "text", text: JSON.stringify(chartData) },
950
- ],
951
- structuredContent: chartData,
952
- };
930
+ return await _buildChartResult(server, chartData, `Fetched and visualizing: ${args.title} (from ${args.url})`);
953
931
  }
954
932
  catch (err) {
955
933
  return {
@@ -964,7 +942,7 @@ export function createServer() {
964
942
  filename: z.string().describe("Filename with extension (e.g. chart.png)"),
965
943
  data: z.string().describe("File contents: base64-encoded binary or plain text"),
966
944
  encoding: z.enum(["base64", "utf-8"]).describe("How data is encoded"),
967
- }, async (args) => {
945
+ }, { readOnlyHint: false, destructiveHint: false, idempotentHint: true }, async (args) => {
968
946
  try {
969
947
  const sanitized = path.basename(args.filename);
970
948
  const downloadsDir = path.join(os.homedir(), "Downloads");
@@ -1341,7 +1319,7 @@ export function createServer() {
1341
1319
  headers: z.record(z.string(), z.string()).optional().describe("Extra HTTP headers (public APIs only - NEVER put API keys here)"),
1342
1320
  method: z.enum(["GET", "POST"]).optional().describe("HTTP method. Default: GET"),
1343
1321
  body: z.string().optional().describe("Request body for POST requests"),
1344
- }, async (args) => {
1322
+ }, { readOnlyHint: true, destructiveHint: false, openWorldHint: true }, async (args) => {
1345
1323
  try {
1346
1324
  let fetchUrl;
1347
1325
  let fetchHeaders = {};