@gradio/core 0.15.1 → 0.16.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @gradio/core
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Features
6
+
7
+ - [#10984](https://github.com/gradio-app/gradio/pull/10984) [`8dab577`](https://github.com/gradio-app/gradio/commit/8dab5771c7d952c76f325681dbf364119c91b0b1) - Let Gradio apps also be MCP Servers. Thanks @abidlabs!
8
+
3
9
  ## 0.15.1
4
10
 
5
11
  ### Fixes
@@ -4,6 +4,7 @@ import Clear from "./img/clear.svelte";
4
4
  import { BaseButton } from "@gradio/button";
5
5
  export let root;
6
6
  export let api_count;
7
+ export let current_language = "python";
7
8
  const dispatch = createEventDispatcher();
8
9
  </script>
9
10
 
@@ -16,17 +17,20 @@ const dispatch = createEventDispatcher();
16
17
  </div>
17
18
  </div>
18
19
  <span class="counts">
19
- <BaseButton
20
- size="sm"
21
- variant="secondary"
22
- elem_id="start-api-recorder"
23
- on:click={() => dispatch("close", { api_recorder_visible: true })}
24
- >
25
- <div class="loading-dot self-baseline"></div>
26
- <p class="self-baseline btn-text">API Recorder</p>
27
- </BaseButton>
20
+ {#if current_language !== "mcp"}
21
+ <BaseButton
22
+ size="sm"
23
+ variant="secondary"
24
+ elem_id="start-api-recorder"
25
+ on:click={() => dispatch("close", { api_recorder_visible: true })}
26
+ >
27
+ <div class="loading-dot self-baseline"></div>
28
+ <p class="self-baseline btn-text">API Recorder</p>
29
+ </BaseButton>
30
+ {/if}
28
31
  <p>
29
- <span class="url">{api_count}</span> API endpoint{#if api_count > 1}s{/if}<br
32
+ <span class="url">{api_count}</span>
33
+ {#if current_language !== "mcp"}API endpoint{:else}MCP Tool{/if}{#if api_count > 1}s{/if}<br
30
34
  />
31
35
  </p>
32
36
  </span>
@@ -3,6 +3,7 @@ declare const __propDef: {
3
3
  props: {
4
4
  root: string;
5
5
  api_count: number;
6
+ current_language?: ("python" | "javascript" | "bash" | "mcp") | undefined;
6
7
  };
7
8
  events: {
8
9
  close: CustomEvent<any>;
@@ -6,10 +6,13 @@ import ParametersSnippet from "./ParametersSnippet.svelte";
6
6
  import InstallSnippet from "./InstallSnippet.svelte";
7
7
  import CodeSnippet from "./CodeSnippet.svelte";
8
8
  import RecordingSnippet from "./RecordingSnippet.svelte";
9
+ import CopyButton from "./CopyButton.svelte";
10
+ import { Block } from "@gradio/atoms";
9
11
  import python from "./img/python.svg";
10
12
  import javascript from "./img/javascript.svg";
11
13
  import bash from "./img/bash.svg";
12
14
  import ResponseSnippet from "./ResponseSnippet.svelte";
15
+ import mcp from "./img/mcp.svg";
13
16
  export let dependencies;
14
17
  export let root;
15
18
  export let app;
@@ -32,11 +35,13 @@ if (!root.endsWith("/")) {
32
35
  export let api_calls = [];
33
36
  let current_language = "python";
34
37
  const langs = [
35
- ["python", python],
36
- ["javascript", javascript],
37
- ["bash", bash]
38
+ ["python", "Python", python],
39
+ ["javascript", "JavaScript", javascript],
40
+ ["bash", "cURL", bash],
41
+ ["mcp", "MCP", mcp]
38
42
  ];
39
43
  let is_running = false;
44
+ let mcp_server_active = false;
40
45
  async function get_info() {
41
46
  let response = await fetch(
42
47
  root.replace(/\/$/, "") + app.api_prefix + "/info"
@@ -57,11 +62,36 @@ get_js_info().then((js_api_info) => {
57
62
  js_info = js_api_info;
58
63
  });
59
64
  const dispatch = createEventDispatcher();
65
+ const mcp_server_url = `${root}gradio_api/mcp/sse`;
66
+ let tools = [];
67
+ async function fetchMcpTools() {
68
+ try {
69
+ const response = await fetch(`${root}gradio_api/mcp/schema`);
70
+ const schema = await response.json();
71
+ tools = Object.entries(schema).map(([name, tool]) => ({
72
+ name: `${name}`,
73
+ description: tool.description || "",
74
+ parameters: tool.properties || {},
75
+ expanded: false
76
+ }));
77
+ } catch (error) {
78
+ console.error("Failed to fetch MCP tools:", error);
79
+ tools = [];
80
+ }
81
+ }
60
82
  onMount(() => {
61
83
  document.body.style.overflow = "hidden";
62
84
  if ("parentIFrame" in window) {
63
85
  window.parentIFrame?.scrollTo(0, 0);
64
86
  }
87
+ fetch(mcp_server_url).then((response) => {
88
+ mcp_server_active = response.ok;
89
+ if (mcp_server_active) {
90
+ fetchMcpTools();
91
+ }
92
+ }).catch(() => {
93
+ mcp_server_active = false;
94
+ });
65
95
  return () => {
66
96
  document.body.style.overflow = "auto";
67
97
  };
@@ -71,26 +101,30 @@ onMount(() => {
71
101
  {#if info}
72
102
  {#if api_count}
73
103
  <div class="banner-wrap">
74
- <ApiBanner on:close root={space_id || root} {api_count} />
104
+ <ApiBanner
105
+ on:close
106
+ root={space_id || root}
107
+ {api_count}
108
+ {current_language}
109
+ />
75
110
  </div>
76
111
 
77
112
  <div class="docs-wrap">
78
113
  <div class="client-doc">
79
114
  <p style="font-size: var(--text-lg);">
80
- Choose a language to see the code snippets for interacting with the
81
- API.
115
+ Choose one of the following ways to interact with the API.
82
116
  </p>
83
117
  </div>
84
118
  <div class="endpoint">
85
119
  <div class="snippets">
86
- {#each langs as [language, img]}
120
+ {#each langs as [language, display_name, img]}
87
121
  <li
88
122
  class="snippet
89
123
  {current_language === language ? 'current-lang' : 'inactive-lang'}"
90
124
  on:click={() => (current_language = language)}
91
125
  >
92
126
  <img src={img} alt="" />
93
- {language}
127
+ {display_name}
94
128
  </li>
95
129
  {/each}
96
130
  </div>
@@ -139,100 +173,256 @@ onMount(() => {
139
173
  href={current_language == "python" ? py_docs : js_docs}
140
174
  target="_blank">docs</a
141
175
  >) if you don't already have it installed.
176
+ {:else if current_language == "mcp"}
177
+ {#if mcp_server_active}
178
+ <Block>
179
+ <div class="mcp-url">
180
+ <label
181
+ ><span class="status-indicator active">●</span>MCP Server
182
+ URL</label
183
+ >
184
+ <div class="textbox">
185
+ <input type="text" readonly value={mcp_server_url} />
186
+ <CopyButton code={mcp_server_url} />
187
+ </div>
188
+ </div>
189
+ </Block>
190
+ <p>&nbsp;</p>
191
+ <strong>Available MCP Tools</strong>
192
+ <div class="mcp-tools">
193
+ {#each tools as tool}
194
+ <div class="tool-item">
195
+ <button
196
+ class="tool-header"
197
+ on:click={() => (tool.expanded = !tool.expanded)}
198
+ >
199
+ <span
200
+ ><span class="tool-name">{tool.name}</span> &nbsp;
201
+ <span class="tool-description"
202
+ >{tool.description
203
+ ? tool.description
204
+ : "⚠︎ No description provided in function docstring"}</span
205
+ ></span
206
+ >
207
+ <span class="tool-arrow"
208
+ >{tool.expanded ? "▼" : "▶"}</span
209
+ >
210
+ </button>
211
+ {#if tool.expanded}
212
+ <div class="tool-content">
213
+ {#if Object.keys(tool.parameters).length > 0}
214
+ <div class="tool-parameters">
215
+ {#if Object.keys(tool.parameters).length > 0}
216
+ {#each Object.entries(tool.parameters) as [name, param]}
217
+ <div class="parameter">
218
+ <code>{name}</code>
219
+ <span class="parameter-type"
220
+ >({param.type})</span
221
+ >
222
+ <p class="parameter-description">
223
+ {param.description
224
+ ? param.description
225
+ : "⚠︎ No description for this parameter in function docstring"}
226
+ </p>
227
+ </div>
228
+ {/each}
229
+ {:else}
230
+ <p>No parameters</p>
231
+ {/if}
232
+ </div>
233
+ {/if}
234
+ </div>
235
+ {/if}
236
+ </div>
237
+ {/each}
238
+ </div>
239
+ <p>&nbsp;</p>
240
+
241
+ <strong>Integration</strong>: To add this MCP to clients that
242
+ support SSE (e.g. Cursor, Windsurf, Cline), simply add the
243
+ following configuration to your MCP config:
244
+ <p>&nbsp;</p>
245
+ <Block>
246
+ <code>
247
+ <div class="copy">
248
+ <CopyButton
249
+ code={JSON.stringify(
250
+ {
251
+ mcpServers: {
252
+ gradio: {
253
+ url: mcp_server_url
254
+ }
255
+ }
256
+ },
257
+ null,
258
+ 2
259
+ )}
260
+ />
261
+ </div>
262
+ <div>
263
+ <pre>{JSON.stringify(
264
+ {
265
+ mcpServers: {
266
+ gradio: {
267
+ url: mcp_server_url
268
+ }
269
+ }
270
+ },
271
+ null,
272
+ 2
273
+ )}</pre>
274
+ </div>
275
+ </code>
276
+ </Block>
277
+ <p>&nbsp;</p>
278
+ <em>Experimental stdio support</em>: For clients that only
279
+ support stdio, first
280
+ <a href="https://nodejs.org/en/download/" target="_blank"
281
+ >install Node.js</a
282
+ >. Then, you can use the following command:
283
+ <p>&nbsp;</p>
284
+ <Block>
285
+ <code>
286
+ <div class="copy">
287
+ <CopyButton
288
+ code={JSON.stringify(
289
+ {
290
+ mcpServers: {
291
+ gradio: {
292
+ command: "npx",
293
+ args: ["mcp-remote", mcp_server_url]
294
+ }
295
+ }
296
+ },
297
+ null,
298
+ 2
299
+ )}
300
+ />
301
+ </div>
302
+ <div>
303
+ <pre>{JSON.stringify(
304
+ {
305
+ mcpServers: {
306
+ gradio: {
307
+ command: "npx",
308
+ arguments: ["mcp-remote", mcp_server_url]
309
+ }
310
+ }
311
+ },
312
+ null,
313
+ 2
314
+ )}</pre>
315
+ </div>
316
+ </code>
317
+ </Block>
318
+ <p>&nbsp;</p>
319
+ <p>&nbsp;</p>
320
+ {:else}
321
+ This Gradio app can also serve as an MCP server, with an MCP
322
+ tool corresponding to each API endpoint. To enable this, launch
323
+ this Gradio app with <code>.launch(mcp_server=True)</code> or
324
+ set the <code>GRADIO_MCP_SERVER</code> env variable to
325
+ <code>"True"</code>.
326
+ {/if}
142
327
  {:else}
143
328
  1. Confirm that you have cURL installed on your system.
144
329
  {/if}
145
330
  </p>
146
331
 
147
- <InstallSnippet {current_language} />
332
+ {#if current_language !== "mcp"}
333
+ <InstallSnippet {current_language} />
148
334
 
149
- <p class="padded">
150
- 2. Find the API endpoint below corresponding to your desired
151
- function in the app. Copy the code snippet, replacing the
152
- placeholder values with your own input data.
153
- {#if space_id}If this is a private Space, you may need to pass your
154
- Hugging Face token as well (<a
155
- href={current_language == "python"
156
- ? py_docs + spaces_docs_suffix
157
- : current_language == "javascript"
158
- ? js_docs + spaces_docs_suffix
159
- : bash_docs}
160
- class="underline"
161
- target="_blank">read more</a
162
- >).{/if}
163
-
164
- Or use the
165
- <Button
166
- size="sm"
167
- variant="secondary"
168
- on:click={() => dispatch("close", { api_recorder_visible: true })}
169
- >
170
- <div class="loading-dot"></div>
171
- <p class="self-baseline">API Recorder</p>
172
- </Button>
173
- to automatically generate your API requests.
174
- {#if current_language == "bash"}<br />&nbsp;<br />Making a
175
- prediction and getting a result requires
176
- <strong>2 requests</strong>: a
177
- <code>POST</code>
178
- and a <code>GET</code> request. The <code>POST</code> request
179
- returns an <code>EVENT_ID</code>, which is used in the second
180
- <code>GET</code> request to fetch the results. In these snippets,
181
- we've used <code>awk</code> and <code>read</code> to parse the
182
- results, combining these two requests into one command for ease of
183
- use. {#if username !== null}
184
- Note: connecting to an authenticated app requires an additional
185
- request.{/if} See
186
- <a href={bash_docs} target="_blank">curl docs</a>.
187
- {/if}
335
+ <p class="padded">
336
+ 2. Find the API endpoint below corresponding to your desired
337
+ function in the app. Copy the code snippet, replacing the
338
+ placeholder values with your own input data.
339
+ {#if space_id}If this is a private Space, you may need to pass
340
+ your Hugging Face token as well (<a
341
+ href={current_language == "python"
342
+ ? py_docs + spaces_docs_suffix
343
+ : current_language == "javascript"
344
+ ? js_docs + spaces_docs_suffix
345
+ : bash_docs}
346
+ class="underline"
347
+ target="_blank">read more</a
348
+ >).{/if}
188
349
 
189
- <!-- <span
350
+ Or use the
351
+ <Button
352
+ size="sm"
353
+ variant="secondary"
354
+ on:click={() =>
355
+ dispatch("close", { api_recorder_visible: true })}
356
+ >
357
+ <div class="loading-dot"></div>
358
+ <p class="self-baseline">API Recorder</p>
359
+ </Button>
360
+ to automatically generate your API requests.
361
+ {#if current_language == "bash"}<br />&nbsp;<br />Making a
362
+ prediction and getting a result requires
363
+ <strong>2 requests</strong>: a
364
+ <code>POST</code>
365
+ and a <code>GET</code> request. The <code>POST</code> request
366
+ returns an <code>EVENT_ID</code>, which is used in the second
367
+ <code>GET</code> request to fetch the results. In these
368
+ snippets, we've used <code>awk</code> and <code>read</code> to
369
+ parse the results, combining these two requests into one command
370
+ for ease of use. {#if username !== null}
371
+ Note: connecting to an authenticated app requires an
372
+ additional request.{/if} See
373
+ <a href={bash_docs} target="_blank">curl docs</a>.
374
+ {/if}
375
+
376
+ <!-- <span
190
377
  id="api-recorder"
191
378
  on:click={() => dispatch("close", { api_recorder_visible: true })}
192
379
  >🪄 API Recorder</span
193
380
  > to automatically generate your API requests! -->
194
- </p>
381
+ </p>
382
+ {/if}
195
383
  {/if}
196
384
 
197
- {#each dependencies as dependency, dependency_index}
198
- {#if dependency.show_api && info.named_endpoints["/" + dependency.api_name]}
199
- <div class="endpoint-container">
200
- <CodeSnippet
201
- named={true}
202
- endpoint_parameters={info.named_endpoints[
203
- "/" + dependency.api_name
204
- ].parameters}
205
- {dependency}
206
- {dependency_index}
207
- {current_language}
208
- {root}
209
- {space_id}
210
- {username}
211
- api_prefix={app.api_prefix}
212
- />
213
-
214
- <ParametersSnippet
215
- endpoint_returns={info.named_endpoints[
216
- "/" + dependency.api_name
217
- ].parameters}
218
- js_returns={js_info.named_endpoints["/" + dependency.api_name]
219
- .parameters}
220
- {is_running}
221
- {current_language}
222
- />
223
-
224
- <ResponseSnippet
225
- endpoint_returns={info.named_endpoints[
226
- "/" + dependency.api_name
227
- ].returns}
228
- js_returns={js_info.named_endpoints["/" + dependency.api_name]
229
- .returns}
230
- {is_running}
231
- {current_language}
232
- />
233
- </div>
234
- {/if}
235
- {/each}
385
+ {#if current_language !== "mcp"}
386
+ {#each dependencies as dependency, dependency_index}
387
+ {#if dependency.show_api && info.named_endpoints["/" + dependency.api_name]}
388
+ <div class="endpoint-container">
389
+ <CodeSnippet
390
+ named={true}
391
+ endpoint_parameters={info.named_endpoints[
392
+ "/" + dependency.api_name
393
+ ].parameters}
394
+ {dependency}
395
+ {dependency_index}
396
+ {current_language}
397
+ {root}
398
+ {space_id}
399
+ {username}
400
+ api_prefix={app.api_prefix}
401
+ />
402
+
403
+ <ParametersSnippet
404
+ endpoint_returns={info.named_endpoints[
405
+ "/" + dependency.api_name
406
+ ].parameters}
407
+ js_returns={js_info.named_endpoints["/" + dependency.api_name]
408
+ .parameters}
409
+ {is_running}
410
+ {current_language}
411
+ />
412
+
413
+ <ResponseSnippet
414
+ endpoint_returns={info.named_endpoints[
415
+ "/" + dependency.api_name
416
+ ].returns}
417
+ js_returns={js_info.named_endpoints["/" + dependency.api_name]
418
+ .returns}
419
+ {is_running}
420
+ {current_language}
421
+ />
422
+ </div>
423
+ {/if}
424
+ {/each}
425
+ {/if}
236
426
  </div>
237
427
  </div>
238
428
  {:else}
@@ -306,7 +496,6 @@ onMount(() => {
306
496
  color: var(--body-text-color);
307
497
  line-height: 1;
308
498
  user-select: none;
309
- text-transform: capitalize;
310
499
  }
311
500
 
312
501
  .current-lang {
@@ -389,4 +578,173 @@ onMount(() => {
389
578
  font-family: var(--font-mono);
390
579
  font-size: var(--text-md);
391
580
  }
581
+
582
+ code pre {
583
+ overflow-x: auto;
584
+ color: var(--body-text-color);
585
+ font-family: var(--font-mono);
586
+ tab-size: 2;
587
+ }
588
+
589
+ .token.string {
590
+ display: contents;
591
+ color: var(--color-accent-base);
592
+ }
593
+
594
+ .copy {
595
+ position: absolute;
596
+ top: 0;
597
+ right: 0;
598
+ margin-top: 5px;
599
+ margin-right: 5px;
600
+ z-index: 10;
601
+ }
602
+
603
+ .container {
604
+ display: flex;
605
+ flex-direction: column;
606
+ gap: var(--spacing-xxl);
607
+ margin-top: var(--size-3);
608
+ margin-bottom: var(--size-3);
609
+ }
610
+
611
+ .desc {
612
+ color: var(--body-text-color-subdued);
613
+ }
614
+
615
+ .api-name {
616
+ color: var(--color-accent);
617
+ }
618
+
619
+ .mcp-url {
620
+ padding: var(--size-2);
621
+ position: relative;
622
+ }
623
+
624
+ .mcp-url label {
625
+ display: block;
626
+ margin-bottom: var(--size-2);
627
+ font-weight: 600;
628
+ color: var(--body-text-color);
629
+ }
630
+
631
+ .mcp-url .textbox {
632
+ display: flex;
633
+ align-items: center;
634
+ gap: var(--size-2);
635
+ border: 1px solid var(--border-color-primary);
636
+ border-radius: var(--radius-sm);
637
+ padding: var(--size-2);
638
+ background: var(--background-fill-primary);
639
+ }
640
+
641
+ .mcp-url input {
642
+ flex: 1;
643
+ border: none;
644
+ background: none;
645
+ color: var(--body-text-color);
646
+ font-family: var(--font-mono);
647
+ font-size: var(--text-md);
648
+ width: 100%;
649
+ }
650
+
651
+ .mcp-url input:focus {
652
+ outline: none;
653
+ }
654
+
655
+ .status-indicator {
656
+ display: inline-block;
657
+ margin-right: var(--size-1-5);
658
+ position: relative;
659
+ top: -1px;
660
+ font-size: 0.8em;
661
+ }
662
+
663
+ .status-indicator.active {
664
+ color: #4caf50;
665
+ animation: pulse 1s infinite;
666
+ }
667
+
668
+ @keyframes pulse {
669
+ 0% {
670
+ opacity: 1;
671
+ }
672
+ 50% {
673
+ opacity: 0.6;
674
+ }
675
+ 100% {
676
+ opacity: 1;
677
+ }
678
+ }
679
+
680
+ .mcp-tools {
681
+ margin-top: var(--size-4);
682
+ border: 1px solid var(--border-color-primary);
683
+ border-radius: var(--radius-md);
684
+ overflow: hidden;
685
+ }
686
+
687
+ .tool-item {
688
+ border-bottom: 1px solid var(--border-color-primary);
689
+ }
690
+
691
+ .tool-item:last-child {
692
+ border-bottom: none;
693
+ }
694
+
695
+ .tool-header {
696
+ width: 100%;
697
+ display: flex;
698
+ justify-content: space-between;
699
+ align-items: center;
700
+ padding: var(--size-3);
701
+ background: var(--background-fill-primary);
702
+ border: none;
703
+ cursor: pointer;
704
+ text-align: left;
705
+ }
706
+
707
+ .tool-header:hover {
708
+ background: var(--background-fill-secondary);
709
+ }
710
+
711
+ .tool-name {
712
+ font-family: var(--font-mono);
713
+ font-weight: 600;
714
+ }
715
+
716
+ .tool-arrow {
717
+ color: var(--body-text-color-subdued);
718
+ }
719
+
720
+ .tool-content {
721
+ padding: var(--size-3);
722
+ background: var(--background-fill-secondary);
723
+ }
724
+
725
+ .tool-description {
726
+ margin-bottom: var(--size-3);
727
+ color: var(--body-text-color);
728
+ }
729
+ .parameter {
730
+ margin-bottom: var(--size-2);
731
+ padding: var(--size-2);
732
+ background: var(--background-fill-primary);
733
+ border-radius: var(--radius-sm);
734
+ }
735
+
736
+ .parameter code {
737
+ font-weight: 600;
738
+ color: var(--color-accent);
739
+ }
740
+
741
+ .parameter-type {
742
+ color: var(--body-text-color-subdued);
743
+ margin-left: var(--size-1);
744
+ }
745
+
746
+ .parameter-description {
747
+ margin-top: var(--size-1);
748
+ color: var(--body-text-color);
749
+ }
392
750
  </style>
@@ -1,7 +1,7 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  declare const __propDef: {
3
3
  props: {
4
- current_language: "python" | "javascript" | "bash";
4
+ current_language: "python" | "javascript" | "bash" | "mcp";
5
5
  };
6
6
  events: {
7
7
  [evt: string]: CustomEvent<any>;