Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .fieldflow/inspect/fde3fce0530fad3c.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"manifest_version": 1,
"command": [
"gh",
"issue",
"list",
"--repo",
"mnfst/modelparams.dev",
"--state",
"open",
"--limit",
"50",
"--json",
"number,title,labels,url,updatedAt,createdAt,body"
],
"command_hash": "fde3fce0530fad3c",
"root_type": "list",
"paths": [
{
"path": "[]",
"types": ["object"]
},
{
"path": "[].body",
"types": ["string"]
},
{
"path": "[].createdAt",
"types": ["string"]
},
{
"path": "[].labels",
"types": ["list"]
},
{
"path": "[].labels[]",
"types": ["object"]
},
{
"path": "[].labels[].color",
"types": ["string"]
},
{
"path": "[].labels[].description",
"types": ["string"]
},
{
"path": "[].labels[].id",
"types": ["string"]
},
{
"path": "[].labels[].name",
"types": ["string"]
},
{
"path": "[].number",
"types": ["integer"]
},
{
"path": "[].title",
"types": ["string"]
},
{
"path": "[].updatedAt",
"types": ["string"]
},
{
"path": "[].url",
"types": ["string"]
}
],
"path_count": 13,
"input_items": 8,
"sampled_items": 8
}
3 changes: 2 additions & 1 deletion api/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export default {

// A GET is either an MCP client opening the optional server-to-client
// stream, which a stateless server has nothing to put on, or a person
// opening the URL. Answer each in its own terms.
// opening the URL. Answer each in its own terms: event-stream clients get
// a 405, everyone else gets the JSON usage object.
if (request.method === "GET") {
if ((request.headers.get("accept") ?? "").includes("text/event-stream")) {
return json({ error: "streaming_not_supported", usage: USAGE }, 405, { Allow: "POST" });
Expand Down
4 changes: 4 additions & 0 deletions src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
API_PATH,
DISAMBIGUATION_PATH,
GLOSSARY_PATH,
MCP_PATH,
modelPagePath,
parameterPagePath,
providerPagePath,
Expand All @@ -36,6 +37,7 @@ import { renderIndex } from "./render.js";
import { renderApiPage } from "./render-api.js";
import { renderDisambiguationPage } from "./render-disambiguation.js";
import { renderGlossaryPage } from "./render-glossary.js";
import { renderMcpPage } from "./render-mcp.js";
import { renderModelPage } from "./render-model.js";
import { defaultSummary,renderParameterPage, rangeSummary } from "./render-parameter.js";
import { renderNotFoundPage } from "./render-not-found.js";
Expand Down Expand Up @@ -80,6 +82,7 @@ async function writeRobotsAndSitemap(models: Model[]): Promise<void> {
{ path: GLOSSARY_PATH, priority: "0.7", lastmod: freshest(models) },
{ path: DISAMBIGUATION_PATH, priority: "0.6", lastmod: freshest(models) },
{ path: API_PATH, priority: "0.5", lastmod: freshest(models) },
{ path: MCP_PATH, priority: "0.5", lastmod: freshest(models) },
...uniqueProviders(models).map((provider) => ({
path: providerPagePath(provider),
priority: "0.8",
Expand Down Expand Up @@ -133,6 +136,7 @@ async function writeHtmlPages(models: Model[]): Promise<void> {
"utf8",
);
await fs.writeFile(path.join(DIST_DIR, "api.html"), await renderApiPage(models), "utf8");
await fs.writeFile(path.join(DIST_DIR, "mcp-server.html"), await renderMcpPage(models), "utf8");
await fs.writeFile(path.join(DIST_DIR, "404.html"), await renderNotFoundPage(models), "utf8");
}

Expand Down
43 changes: 43 additions & 0 deletions src/build/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/** Escape HTML special characters so highlighted JSON can be injected into markup. */
function escapeHtml(value: string): string {
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

/**
* Minimal JSON syntax highlighter. Returns HTML where each token is wrapped in a
* span with a Tailwind text color, tuned for the site's dark code blocks
* (`bg-slate-900`). Punctuation is left in the surrounding text color.
*
* Colours: keys sky, strings emerald, numbers violet, booleans amber, null rose.
*/
export function highlightJson(json: string): string {
const token =
/("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g;

return json.replace(token, (match) => {
let cls = "text-violet-300";
if (/^"/.test(match)) {
cls = /:$/.test(match) ? "text-sky-300" : "text-emerald-300";
} else if (/^(true|false)$/.test(match)) {
cls = "text-amber-300";
} else if (match === "null") {
cls = "text-rose-300";
}
return `<span class="${cls}">${escapeHtml(match)}</span>`;
});
}

/**
* A self-contained code block for JSON: a header bar with a label and a Copy
* button, and a body that wraps long lines instead of overflowing. The copy
* button is wired by `[data-json-copy]`; it copies the sibling `<code>` text.
*/
export function jsonBlock(json: string, label = "JSON"): string {
return `<figure>
<div class="flex items-center justify-between rounded-t-md border border-slate-200 border-b-0 bg-slate-800 px-4 py-1.5 dark:border-[hsla(60,2%,12%,0.17)] dark:bg-[#161616]">
<span class="font-mono text-xs font-medium text-slate-400">${escapeHtml(label)}</span>
<button type="button" data-json-copy class="font-mono text-xs font-medium text-slate-400 hover:text-white">Copy</button>
</div>
<pre class="rounded-b-md border border-slate-200 bg-slate-900 px-5 py-4 font-mono text-sm leading-relaxed text-slate-200 whitespace-pre-wrap break-words dark:border-[hsla(60,2%,12%,0.17)] dark:bg-[#0d0d0d]"><code>${highlightJson(json)}</code></pre>
</figure>`;
}
34 changes: 34 additions & 0 deletions src/build/render-mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import path from "node:path";
import ejs from "ejs";
import { MCP_CLIENTS, MCP_TOOLS } from "../data/mcp.js";
import { VIEWS_DIR } from "../data/paths.js";
import { SITE_NAME, SITE_URL } from "../data/site.js";
import { MCP_PATH, absolute, ogImagePath } from "../data/urls.js";
import { type Model } from "../schema/model.js";
import { jsonBlock } from "./highlight.js";
import { hubLinks, renderShell, viewHelpers } from "./render.js";

const MCP_TITLE = `MCP server · ${SITE_NAME}`;
const MCP_DESCRIPTION =
"A Model Context Protocol server for the modelparams.dev catalog — let a coding agent check which parameters a model accepts before it calls one.";

export async function renderMcpPage(allModels: Model[]): Promise<string> {
const body = await ejs.renderFile(path.join(VIEWS_DIR, "mcp.ejs"), {
clients: MCP_CLIENTS,
tools: MCP_TOOLS,
jsonBlock,
helpers: viewHelpers,
});

return renderShell(
{
title: MCP_TITLE,
description: MCP_DESCRIPTION,
canonicalUrl: absolute(SITE_URL, MCP_PATH),
ogImage: ogImagePath(MCP_PATH),
structuredData: "{}",
providerHubs: hubLinks(allModels),
},
body,
);
}
2 changes: 2 additions & 0 deletions src/build/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "../data/urls.js";
import { modelId, type Catalog, type Model } from "../schema/model.js";
import { fitDescription, fitTitle } from "./meta.js";
import { highlightJson } from "./highlight.js";
import { buildHomeStructuredData } from "./structured-data.js";

const LAYOUT_PATH = path.join(VIEWS_DIR, "layout.ejs");
Expand All @@ -49,6 +50,7 @@ export const viewHelpers = {
parameterPagePath,
parameterAnchorId,
providerPagePath,
highlightJson,
};

export interface HubLink {
Expand Down
72 changes: 72 additions & 0 deletions src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,76 @@ function setupCopyNpm(): void {
});
}

function setupMcpClients(): void {
const buttons = document.querySelectorAll<HTMLButtonElement>("[data-mcp-client]");
const panels = document.querySelectorAll<HTMLElement>("[data-mcp-panel]");
if (buttons.length === 0) return;

const activeClass = [
"border-slate-900",
"bg-slate-900",
"text-white",
"dark:border-white",
"dark:bg-white",
"dark:text-slate-900",
];
const idleClass = [
"border-slate-300",
"text-slate-700",
"hover:border-slate-500",
"dark:border-slate-700",
"dark:text-slate-300",
];

buttons.forEach((button) => {
button.addEventListener("click", () => {
const id = button.dataset.mcpClient;
buttons.forEach((b) => {
const on = b === button;
b.classList.remove(...(on ? idleClass : activeClass));
b.classList.add(...(on ? activeClass : idleClass));
});
panels.forEach((panel) => panel.classList.toggle("hidden", panel.dataset.mcpPanel !== id));
});
});

const copyButtons = document.querySelectorAll<HTMLButtonElement>("[data-copy-mcp]");
copyButtons.forEach((button) => {
const idle = button.querySelector<HTMLElement>("[data-copy-mcp-idle]");
const done = button.querySelector<HTMLElement>("[data-copy-mcp-done]");
let timer = 0;
button.addEventListener("click", async () => {
const code = document.querySelector<HTMLElement>(
`[data-mcp-command="${button.dataset.copyMcp}"]`,
);
if (!code) return;
await copyText(code.textContent?.trim() ?? "");
idle?.classList.add("hidden");
done?.classList.remove("hidden");
window.clearTimeout(timer);
timer = window.setTimeout(() => {
idle?.classList.remove("hidden");
done?.classList.add("hidden");
}, 2000);
});
});
}

function setupJsonCopy(): void {
document.querySelectorAll<HTMLButtonElement>("[data-json-copy]").forEach((button) => {
button.addEventListener("click", async () => {
const code = button.closest("figure")?.querySelector("code");
if (!code) return;
await copyText(code.textContent?.trim() ?? "");
const original = button.textContent;
button.textContent = "Copied";
window.setTimeout(() => {
button.textContent = original;
}, 2000);
});
});
}

function setupThemeToggle(): void {
const toggle = document.querySelector<HTMLButtonElement>("[data-theme-toggle]");
if (!toggle) return;
Expand Down Expand Up @@ -534,6 +604,8 @@ function setupMobileMenu(): void {
document.addEventListener("DOMContentLoaded", () => {
setupThemeToggle();
setupCopyNpm();
setupMcpClients();
setupJsonCopy();
setupHowToUseModal();
setupCopyHowToUse();
setupProvidersMenu();
Expand Down
Loading
Loading