usmar commited on
Commit
9d89547
·
verified ·
1 Parent(s): 5b6d6ec

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +12 -5
  2. index.html +283 -18
  3. shim.js +118 -0
README.md CHANGED
@@ -1,10 +1,17 @@
1
  ---
2
- title: Sodabar
3
- emoji: 👀
4
- colorFrom: gray
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: sodabar
3
+ emoji: 🥤
4
+ colorFrom: blue
5
+ colorTo: yellow
6
  sdk: static
7
  pinned: false
8
+ license: mit
9
+ short_description: NYC Open Data on tap — MCP tool console demo
10
  ---
11
 
12
+ # sodabar open data on tap
13
+
14
+ Static demo of the [sodabar](https://github.com/UsmarHaider/sodabar) MCP
15
+ server's tool console. The four MCP tools are answered **in your browser**,
16
+ straight from the live NYC Open Data APIs (Socrata sends CORS `*`).
17
+ The full project — MCP server, FastAPI playground, tests — lives on GitHub.
index.html CHANGED
@@ -1,19 +1,284 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>sodabar open data on tap</title>
7
+ <style>
8
+ :root {
9
+ --bg: #fcfcfb; --panel: #ffffff; --ink: #1c2733; --ink2: #5a6a7a; --ink3: #8b98a5;
10
+ --line: #e3e8ee; --blue: #2a78d6; --orange: #eb6834; --blue-soft: #eaf2fc;
11
+ --chip: #f1f4f8; --code-bg: #f6f8fa; --ok: #1a7f4e;
12
+ }
13
+ @media (prefers-color-scheme: dark) {
14
+ :root {
15
+ --bg: #15191f; --panel: #1c222b; --ink: #e8edf2; --ink2: #a7b3c0; --ink3: #6f7d8c;
16
+ --line: #2c3540; --blue: #4a8fd9; --orange: #e8632f; --blue-soft: #1e2c3d;
17
+ --chip: #242c37; --code-bg: #12161c; --ok: #3dbf7f;
18
+ }
19
+ }
20
+ * { box-sizing: border-box; margin: 0; }
21
+ body {
22
+ background: var(--bg); color: var(--ink);
23
+ font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
24
+ padding: 18px 22px;
25
+ }
26
+ header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 14px; }
27
+ h1 { font-size: 21px; letter-spacing: -0.02em; }
28
+ h1 .cup { color: var(--orange); }
29
+ .tag { color: var(--ink2); font-size: 13px; }
30
+ .live { margin-left: auto; font-size: 12px; color: var(--ok); white-space: nowrap; }
31
+ .live::before { content: "●"; margin-right: 5px; }
32
+ .grid { display: grid; grid-template-columns: 320px 1fr; gap: 14px; align-items: start; }
33
+ @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } }
34
+ .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
35
+ .tools { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 12px; }
36
+ .tool {
37
+ border: 1px solid var(--line); border-radius: 8px; padding: 7px 9px; cursor: pointer;
38
+ background: none; color: var(--ink2); font: inherit; font-size: 12.5px; text-align: left;
39
+ }
40
+ .tool code { font-size: 11.5px; }
41
+ .tool.on { border-color: var(--blue); background: var(--blue-soft); color: var(--ink); }
42
+ label { display: block; font-size: 11.5px; color: var(--ink2); margin: 8px 0 3px; }
43
+ input, textarea {
44
+ width: 100%; border: 1px solid var(--line); border-radius: 7px; padding: 6px 8px;
45
+ background: var(--bg); color: var(--ink); font: 12.5px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
46
+ }
47
+ input:focus, textarea:focus { outline: 2px solid var(--blue); outline-offset: -1px; border-color: transparent; }
48
+ .row { display: flex; gap: 8px; margin-top: 12px; }
49
+ .btn {
50
+ border: 0; border-radius: 8px; padding: 8px 14px; font: 600 13px/1 inherit;
51
+ cursor: pointer; font-family: inherit;
52
+ }
53
+ .btn.run { background: var(--blue); color: #fff; flex: 1; }
54
+ .btn.sample { background: var(--chip); color: var(--ink); }
55
+ .btn:active { transform: translateY(1px); }
56
+ .callbox { margin-bottom: 10px; }
57
+ .callbox .lbl, .out .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink3); margin-bottom: 5px; }
58
+ pre.call {
59
+ background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px;
60
+ padding: 9px 11px; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
61
+ overflow-x: auto; color: var(--ink2);
62
+ }
63
+ pre.call .k { color: var(--blue); }
64
+ pre.call .v { color: var(--orange); }
65
+ .meta { font-size: 12px; color: var(--ink2); margin: 8px 0 6px; }
66
+ .meta b { color: var(--ink); }
67
+ table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
68
+ th, td { text-align: left; padding: 5px 10px 5px 0; border-bottom: 1px solid var(--line); }
69
+ th { color: var(--ink2); font-weight: 600; font-size: 11.5px; }
70
+ td { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
71
+ td.num { text-align: right; font-variant-numeric: tabular-nums; }
72
+ th.num { text-align: right; }
73
+ .chart { margin: 10px 0 4px; }
74
+ .bar-row { display: grid; grid-template-columns: 120px 1fr 70px; gap: 8px; align-items: center; margin-bottom: 4px; }
75
+ .bar-label { font-size: 12px; color: var(--ink2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
76
+ .bar-track { height: 14px; position: relative; }
77
+ .bar-fill { height: 100%; background: var(--blue); border-radius: 0 4px 4px 0; min-width: 2px; }
78
+ .bar-row:hover .bar-fill { background: var(--orange); }
79
+ .bar-val { font-size: 11.5px; color: var(--ink); font-variant-numeric: tabular-nums; }
80
+ .err { color: var(--orange); font-size: 13px; padding: 6px 0; }
81
+ .empty { color: var(--ink3); font-size: 13px; padding: 20px 0; text-align: center; }
82
+ .scroll { max-height: 300px; overflow: auto; }
83
+ footer { margin-top: 12px; font-size: 11.5px; color: var(--ink3); }
84
+ footer a { color: var(--blue); text-decoration: none; }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <header>
89
+ <h1>sodabar<span class="cup"> ▙</span></h1>
90
+ <span class="tag">open data on tap — the MCP tool console</span>
91
+ <span class="live" id="live">data.cityofnewyork.us</span>
92
+ </header>
93
+
94
+ <div class="grid">
95
+ <div class="panel">
96
+ <div class="tools" id="tools"></div>
97
+ <form id="form" onsubmit="return runTool(event)"><div id="fields"></div>
98
+ <div class="row">
99
+ <button class="btn run" type="submit">Call tool</button>
100
+ <button class="btn sample" type="button" onclick="loadSample()">Try a sample</button>
101
+ </div>
102
+ </form>
103
+ </div>
104
+
105
+ <div class="panel">
106
+ <div class="callbox"><div class="lbl">tools/call</div><pre class="call" id="call">{ }</pre></div>
107
+ <div class="out">
108
+ <div class="lbl">result</div>
109
+ <div id="meta" class="meta"></div>
110
+ <div id="result"><div class="empty">Pick a tool and call it — or press “Try a sample”.</div></div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <footer>Four MCP tools · SoQL queries capped at 1000 rows · same responses an LLM client sees</footer>
116
+
117
+ <script src="shim.js"></script>
118
+ <script>
119
+ const TOOLS = {
120
+ query_dataset: {
121
+ fields: [
122
+ ["dataset_id", "erm2-nwe9"], ["select", ""], ["where", ""],
123
+ ["group", ""], ["order", ""], ["limit", "50"],
124
+ ],
125
+ endpoint: (a) => ["/api/query", a],
126
+ },
127
+ search_datasets: {
128
+ fields: [["query", "rat sightings"], ["limit", "10"]],
129
+ endpoint: (a) => ["/api/search", { q: a.query, limit: a.limit }],
130
+ },
131
+ get_schema: {
132
+ fields: [["dataset_id", "erm2-nwe9"]],
133
+ endpoint: (a) => [`/api/schema/${encodeURIComponent(a.dataset_id)}`, {}],
134
+ },
135
+ profile_column: {
136
+ fields: [["dataset_id", "erm2-nwe9"], ["column", "borough"], ["top", "10"]],
137
+ endpoint: (a) => ["/api/profile", a],
138
+ },
139
+ };
140
+ let current = "query_dataset";
141
+ let sampleIndex = 0;
142
+
143
+ function renderTools() {
144
+ const box = document.getElementById("tools");
145
+ box.innerHTML = "";
146
+ for (const name of Object.keys(TOOLS)) {
147
+ const b = document.createElement("button");
148
+ b.type = "button";
149
+ b.className = "tool" + (name === current ? " on" : "");
150
+ b.innerHTML = `<code>${name}</code>`;
151
+ b.onclick = () => { current = name; renderTools(); renderFields({}); };
152
+ box.appendChild(b);
153
+ }
154
+ }
155
+ function renderFields(values) {
156
+ const box = document.getElementById("fields");
157
+ box.innerHTML = "";
158
+ for (const [name, placeholder] of TOOLS[current].fields) {
159
+ const l = document.createElement("label");
160
+ l.textContent = name;
161
+ const i = document.createElement("input");
162
+ i.name = name;
163
+ i.placeholder = placeholder;
164
+ if (values[name] !== undefined) i.value = values[name];
165
+ box.append(l, i);
166
+ }
167
+ }
168
+ function formArgs() {
169
+ const args = {};
170
+ for (const input of document.querySelectorAll("#fields input"))
171
+ if (input.value.trim()) args[input.name] = input.value.trim();
172
+ return args;
173
+ }
174
+ function showCall(tool, args) {
175
+ const argsJson = JSON.stringify(args, null, 2).replace(/"([^"]+)":/g, '<span class="k">"$1"</span>:')
176
+ .replace(/: "([^"]*)"/g, ': <span class="v">"$1"</span>');
177
+ document.getElementById("call").innerHTML =
178
+ `{ <span class="k">"name"</span>: <span class="v">"${tool}"</span>, <span class="k">"arguments"</span>: ${argsJson} }`;
179
+ }
180
+ async function runTool(ev) {
181
+ if (ev) ev.preventDefault();
182
+ const args = formArgs();
183
+ showCall(current, args);
184
+ const [path, params] = TOOLS[current].endpoint(args);
185
+ const qs = new URLSearchParams(params).toString();
186
+ const t0 = performance.now();
187
+ const resultBox = document.getElementById("result");
188
+ const meta = document.getElementById("meta");
189
+ meta.textContent = "…";
190
+ resultBox.innerHTML = "";
191
+ try {
192
+ const res = await fetch(path + (qs ? "?" + qs : ""));
193
+ const body = await res.json();
194
+ const ms = Math.round(performance.now() - t0);
195
+ if (!res.ok) throw new Error(body.detail || res.statusText);
196
+ render(body, ms);
197
+ } catch (e) {
198
+ meta.textContent = "";
199
+ resultBox.innerHTML = `<div class="err">✗ ${e.message}</div>`;
200
+ }
201
+ return false;
202
+ }
203
+ function render(body, ms) {
204
+ const meta = document.getElementById("meta");
205
+ const box = document.getElementById("result");
206
+ box.innerHTML = "";
207
+ if (Array.isArray(body)) {
208
+ meta.innerHTML = `<b>${body.length}</b> row${body.length === 1 ? "" : "s"} · ${ms} ms`;
209
+ if (!body.length) { box.innerHTML = '<div class="empty">No rows.</div>'; return; }
210
+ const cols = Object.keys(body[0]);
211
+ if (cols.length === 2 && body.length <= 30 && body.every(r => isFinite(+r[cols[1]]))) {
212
+ box.appendChild(barChart(body, cols));
213
+ }
214
+ box.appendChild(tableOf(body, cols));
215
+ } else if (body && body.columns) {
216
+ meta.innerHTML = `<b>${body.name || body.id}</b> · ${body.columns.length} columns`;
217
+ box.appendChild(tableOf(body.columns.map(c => ({ column: c.field_name, type: c.type })), ["column", "type"]));
218
+ } else {
219
+ meta.textContent = "";
220
+ box.innerHTML = `<pre class="call">${JSON.stringify(body, null, 2)}</pre>`;
221
+ }
222
+ }
223
+ function tableOf(rows, cols) {
224
+ const wrap = document.createElement("div");
225
+ wrap.className = "scroll";
226
+ const numeric = cols.map(c => rows.every(r => r[c] === undefined || isFinite(+r[c])));
227
+ let html = "<table><tr>" + cols.map((c, i) => `<th${numeric[i] ? ' class="num"' : ""}>${c}</th>`).join("") + "</tr>";
228
+ for (const r of rows.slice(0, 200))
229
+ html += "<tr>" + cols.map((c, i) => {
230
+ let v = r[c] === undefined ? "" : String(r[c]);
231
+ if (numeric[i] && v) v = (+v).toLocaleString();
232
+ return `<td${numeric[i] ? ' class="num"' : ""}>${escapeHtml(v).slice(0, 120)}</td>`;
233
+ }).join("") + "</tr>";
234
+ wrap.innerHTML = html + "</table>";
235
+ return wrap;
236
+ }
237
+ function barChart(rows, cols) {
238
+ const box = document.createElement("div");
239
+ box.className = "chart";
240
+ const max = Math.max(...rows.map(r => +r[cols[1]]));
241
+ for (const r of rows.slice(0, 12)) {
242
+ const v = +r[cols[1]];
243
+ const row = document.createElement("div");
244
+ row.className = "bar-row";
245
+ row.title = `${r[cols[0]]}: ${v.toLocaleString()}`;
246
+ row.innerHTML = `<div class="bar-label">${escapeHtml(String(r[cols[0]] ?? "—"))}</div>
247
+ <div class="bar-track"><div class="bar-fill" style="width:${Math.max(1, v / max * 100)}%"></div></div>
248
+ <div class="bar-val">${v.toLocaleString()}</div>`;
249
+ box.appendChild(row);
250
+ }
251
+ return box;
252
+ }
253
+ function escapeHtml(s) {
254
+ return s.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
255
+ }
256
+ async function loadSample() {
257
+ const res = await fetch(`/sample?index=${sampleIndex}`);
258
+ const s = await res.json();
259
+ sampleIndex += 1;
260
+ current = s.tool;
261
+ renderTools();
262
+ const values = {};
263
+ for (const [k, v] of Object.entries(s.args)) values[k === "query" ? "query" : k] = v;
264
+ renderFields(values);
265
+ runTool();
266
+ }
267
+ renderTools();
268
+ renderFields({});
269
+ const qs = new URLSearchParams(location.search);
270
+ if (qs.get("demo")) {
271
+ if (qs.get("err")) {
272
+ // guardrail demo: a malformed tool call, answered with a readable error
273
+ current = "query_dataset";
274
+ renderTools();
275
+ renderFields({ dataset_id: "311-complaints", select: "borough, count(*)" });
276
+ runTool();
277
+ } else {
278
+ sampleIndex = +(qs.get("sample") || 0);
279
+ loadSample();
280
+ }
281
+ }
282
+ </script>
283
+ </body>
284
  </html>
shim.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Static-Space shim: answer the playground's own /api/* and /sample routes
2
+ // in the browser by calling the Socrata APIs directly (they send
3
+ // Access-Control-Allow-Origin: *). This keeps index.html identical to the
4
+ // FastAPI-served version — only the transport changes.
5
+ (function () {
6
+ const DOMAIN = "data.cityofnewyork.us";
7
+ const ID_RE = /^[a-z0-9]{4}-[a-z0-9]{4}$/;
8
+ const MAX_LIMIT = 1000;
9
+
10
+ const SAMPLES = [
11
+ {
12
+ title: "311 complaints by borough, 2026",
13
+ tool: "query_dataset",
14
+ args: {
15
+ dataset_id: "erm2-nwe9",
16
+ select: "borough, count(*) as n",
17
+ where: "created_date > '2026-01-01T00:00:00'",
18
+ group: "borough",
19
+ order: "n DESC",
20
+ },
21
+ },
22
+ { title: "Search the catalog for squirrels", tool: "search_datasets", args: { query: "squirrel census" } },
23
+ {
24
+ title: "Top complaint types this year",
25
+ tool: "profile_column",
26
+ args: { dataset_id: "erm2-nwe9", column: "complaint_type", top: 10 },
27
+ },
28
+ { title: "Schema of the 2015 Street Tree Census", tool: "get_schema", args: { dataset_id: "uvpi-gqnh" } },
29
+ ];
30
+
31
+ const ok = (body) => new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } });
32
+ const fail = (status, detail) => new Response(JSON.stringify({ detail }), { status, headers: { "Content-Type": "application/json" } });
33
+
34
+ function checkId(id) {
35
+ if (!ID_RE.test((id || "").trim().toLowerCase()))
36
+ throw { status: 422, detail: `invalid dataset id '${id}': expected the Socrata 4x4 form, e.g. 'erm2-nwe9'` };
37
+ return id.trim().toLowerCase();
38
+ }
39
+ const clamp = (v, dflt) => Math.max(1, Math.min(parseInt(v || dflt, 10) || dflt, MAX_LIMIT));
40
+
41
+ async function upstream(url) {
42
+ const res = await fetch(url);
43
+ if (!res.ok) {
44
+ let detail = "";
45
+ try { detail = (await res.json()).message || ""; } catch (e) { /* not json */ }
46
+ if (res.status === 404) throw { status: 502, detail: "not found — check the dataset id and domain" };
47
+ if (res.status === 400) throw { status: 502, detail: `the API rejected the query (${detail || "bad request"}) — check your SoQL syntax` };
48
+ throw { status: 502, detail: `HTTP ${res.status} from Socrata: ${detail}` };
49
+ }
50
+ return res.json();
51
+ }
52
+
53
+ async function route(path, params) {
54
+ if (path === "/sample") {
55
+ const i = parseInt(params.get("index") || "0", 10) || 0;
56
+ return SAMPLES[((i % SAMPLES.length) + SAMPLES.length) % SAMPLES.length];
57
+ }
58
+ if (path === "/api/search") {
59
+ const data = await upstream(
60
+ "https://api.us.socrata.com/api/catalog/v1?domains=" + DOMAIN +
61
+ "&only=dataset&q=" + encodeURIComponent(params.get("q") || "") +
62
+ "&limit=" + clamp(params.get("limit"), 10)
63
+ );
64
+ return (data.results || []).map((r) => ({
65
+ id: r.resource.id,
66
+ name: r.resource.name,
67
+ description: (r.resource.description || "").slice(0, 500),
68
+ updated: r.resource.data_updated_at,
69
+ domain: DOMAIN,
70
+ }));
71
+ }
72
+ if (path.startsWith("/api/schema/")) {
73
+ const id = checkId(path.split("/").pop());
74
+ const data = await upstream(`https://${DOMAIN}/api/views/${id}.json`);
75
+ return {
76
+ id,
77
+ domain: DOMAIN,
78
+ name: data.name,
79
+ description: (data.description || "").slice(0, 1000),
80
+ columns: (data.columns || [])
81
+ .filter((c) => !String(c.fieldName || "").startsWith(":"))
82
+ .map((c) => ({ field_name: c.fieldName, type: c.dataTypeName, description: (c.description || "").slice(0, 300) })),
83
+ };
84
+ }
85
+ if (path === "/api/query" || path === "/api/profile") {
86
+ const id = checkId(params.get("dataset_id"));
87
+ const q = new URLSearchParams();
88
+ if (path === "/api/profile") {
89
+ const col = params.get("column") || "";
90
+ q.set("$select", `${col}, count(*) as count`);
91
+ q.set("$group", col);
92
+ q.set("$order", "count DESC");
93
+ q.set("$limit", clamp(params.get("top"), 20));
94
+ } else {
95
+ for (const [key, dollar] of [["select", "$select"], ["where", "$where"], ["group", "$group"], ["order", "$order"]])
96
+ if (params.get(key)) q.set(dollar, params.get(key));
97
+ q.set("$limit", clamp(params.get("limit"), 50));
98
+ if (+params.get("offset")) q.set("$offset", +params.get("offset"));
99
+ }
100
+ return upstream(`https://${DOMAIN}/resource/${id}.json?` + q.toString());
101
+ }
102
+ throw { status: 404, detail: "unknown route " + path };
103
+ }
104
+
105
+ const realFetch = window.fetch.bind(window);
106
+ window.fetch = async function (input, init) {
107
+ const url = typeof input === "string" ? input : input.url;
108
+ if (url.startsWith("/api/") || url.startsWith("/sample")) {
109
+ const u = new URL(url, location.origin);
110
+ try {
111
+ return ok(await route(u.pathname, u.searchParams));
112
+ } catch (e) {
113
+ return fail(e.status || 502, e.detail || String(e));
114
+ }
115
+ }
116
+ return realFetch(input, init);
117
+ };
118
+ })();