Compare commits
2 Commits
21a5cf5762
...
4687c8be52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4687c8be52 | ||
|
|
1349dc1dc0 |
@ -10,9 +10,20 @@ import { getRedis } from './redis.js';
|
|||||||
*/
|
*/
|
||||||
export const stripe: Stripe | null = config.STRIPE_SECRET_KEY
|
export const stripe: Stripe | null = config.STRIPE_SECRET_KEY
|
||||||
? new Stripe(config.STRIPE_SECRET_KEY, {
|
? new Stripe(config.STRIPE_SECRET_KEY, {
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: SDK type lags behind real API version strings
|
// Must match the version the installed SDK (stripe@22) is built against —
|
||||||
apiVersion: '2025-10-29.acacia' as any,
|
// its types expose ui_mode: 'embedded_page', which only exists from this
|
||||||
|
// version on. Pinning the older '2025-10-29.acacia' made Stripe reject the
|
||||||
|
// embedded checkout create call (acacia still used ui_mode: 'embedded').
|
||||||
|
apiVersion: '2026-04-22.dahlia',
|
||||||
typescript: true,
|
typescript: true,
|
||||||
|
// Fail fast + visibly. Without a tight timeout, a wedged Stripe call (bad
|
||||||
|
// version, egress hiccup) hangs past Cloudflare's ~100s edge limit, and
|
||||||
|
// CF returns its own 5xx WITHOUT our CORS headers — which surfaces in the
|
||||||
|
// browser as an opaque "No Access-Control-Allow-Origin" error instead of
|
||||||
|
// the real failure. 20s keeps us well inside the edge limit so the handler
|
||||||
|
// returns a proper 502 (with CORS) the client can actually read.
|
||||||
|
timeout: 20_000,
|
||||||
|
maxNetworkRetries: 2,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|||||||
@ -480,7 +480,11 @@ function NewServerPageInner() {
|
|||||||
/>
|
/>
|
||||||
<p className="text-[12px] leading-relaxed text-[--color-fg-subtle]">
|
<p className="text-[12px] leading-relaxed text-[--color-fg-subtle]">
|
||||||
Next step we'll show you exactly which tools we'll expose and let you tweak
|
Next step we'll show you exactly which tools we'll expose and let you tweak
|
||||||
the spec before we build.
|
the spec before we build.{' '}
|
||||||
|
<span className="text-[--color-fg-muted]">
|
||||||
|
Don't paste API keys or access tokens here — you'll add each one in its own
|
||||||
|
encrypted field in the next step.
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-1.5 pt-1">
|
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||||
{EXAMPLE_PROMPTS.map((p) => (
|
{EXAMPLE_PROMPTS.map((p) => (
|
||||||
@ -675,15 +679,18 @@ function NewServerPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[13px] font-semibold tracking-tight">Credentials we need</h3>
|
<h3 className="text-[13px] font-semibold tracking-tight">API keys & credentials</h3>
|
||||||
<p className="mt-1 text-[12px] leading-relaxed text-[--color-fg-muted]">
|
<p className="mt-1 text-[12px] leading-relaxed text-[--color-fg-muted]">
|
||||||
AES-256-GCM encrypted at rest, injected as env vars at runtime. Remove if your
|
One field per key or access token — entered here, separately from your prompt.
|
||||||
implementation doesn't actually use one.
|
AES-256-GCM encrypted at rest, injected as env vars at runtime only. Remove any your
|
||||||
|
implementation doesn't use; add any we missed.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{editable.requiredSecrets.length === 0 && (
|
{editable.requiredSecrets.length === 0 && (
|
||||||
<p className="text-[12.5px] text-[--color-fg-muted]">
|
<p className="text-[12.5px] text-[--color-fg-muted]">
|
||||||
No credentials. This server runs self-contained.
|
None detected. If your tool calls an API that needs a key or access token, add it
|
||||||
|
below with <span className="mono">+ Add credential</span> — never put secrets in
|
||||||
|
the prompt.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{editable.requiredSecrets.map((key, idx) => (
|
{editable.requiredSecrets.map((key, idx) => (
|
||||||
|
|||||||
73
apps/web/app/(marketing)/guides/article-shell.tsx
Normal file
73
apps/web/app/(marketing)/guides/article-shell.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
// Shared layout + typographic primitives for /guides/* SEO articles. Server
|
||||||
|
// component (no client JS) so each article page can export its own metadata.
|
||||||
|
|
||||||
|
export function ArticleShell({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
updated,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
updated?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<article className="mx-auto max-w-3xl px-6 py-14">
|
||||||
|
<Link
|
||||||
|
href="/guides"
|
||||||
|
className="text-[12px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
|
||||||
|
>
|
||||||
|
← Guides
|
||||||
|
</Link>
|
||||||
|
<h1 className="mt-4 text-[30px] font-semibold leading-tight tracking-tight text-[--color-fg]">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{subtitle && <p className="mt-3 text-[15px] leading-relaxed text-[--color-fg-muted]">{subtitle}</p>}
|
||||||
|
{updated && <p className="mt-2 text-[12px] text-[--color-fg-subtle]">Updated {updated}</p>}
|
||||||
|
<div className="mt-8">{children}</div>
|
||||||
|
|
||||||
|
<div className="mt-14 rounded-lg border border-[--color-border] bg-[--color-bg-subtle] p-5">
|
||||||
|
<p className="text-[14px] font-medium text-[--color-fg]">
|
||||||
|
Skip the boilerplate — describe your tool, get a hosted MCP server.
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[13px] text-[--color-fg-muted]">
|
||||||
|
BuildMyMCPServer generates the TypeScript server, wraps it in OAuth 2.1 and deploys it to a
|
||||||
|
public Streamable HTTP URL for Claude, Cursor and ChatGPT. Free tier, source export, no
|
||||||
|
lock-in.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="mt-3 inline-flex h-9 items-center rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#5557e8]"
|
||||||
|
>
|
||||||
|
Start building →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function H2({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<h2 className="mt-10 text-[19px] font-semibold tracking-tight text-[--color-fg]">{children}</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function P({ children }: { children: ReactNode }) {
|
||||||
|
return <p className="mt-3 text-[14.5px] leading-relaxed text-[--color-fg-muted]">{children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UL({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-[14.5px] leading-relaxed text-[--color-fg-muted]">
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Strong({ children }: { children: ReactNode }) {
|
||||||
|
return <strong className="font-semibold text-[--color-fg]">{children}</strong>;
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
import { articleJsonLd, pageMetadata } from '@/lib/seo';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
|
||||||
|
|
||||||
|
const PATH = '/guides/host-mcp-server-with-oauth';
|
||||||
|
const TITLE = 'How to host a remote MCP server with OAuth (2026)';
|
||||||
|
const DESCRIPTION =
|
||||||
|
'What it actually takes to put a remote MCP server in production: Streamable HTTP transport, OAuth 2.1 with PKCE and Resource Indicators, and the shortcuts.';
|
||||||
|
|
||||||
|
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonLd
|
||||||
|
data={articleJsonLd({
|
||||||
|
title: TITLE,
|
||||||
|
description: DESCRIPTION,
|
||||||
|
path: PATH,
|
||||||
|
datePublished: '2026-05-31',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<ArticleShell
|
||||||
|
title={TITLE}
|
||||||
|
subtitle="Local STDIO servers are easy. A remote MCP server that Claude, Cursor and ChatGPT can install over the internet — without leaving it open to the world — is where the real work is. Here's the whole picture."
|
||||||
|
updated="May 2026"
|
||||||
|
>
|
||||||
|
<H2>Local vs remote: why this is harder than it looks</H2>
|
||||||
|
<P>
|
||||||
|
A local MCP server talks to one client over STDIO on your machine — no network, no auth.
|
||||||
|
The moment you want a server that lives at a URL and any MCP client can connect to, you
|
||||||
|
inherit a full web-service problem: a public transport, TLS, identity, authorization, and
|
||||||
|
isolation between callers. The MCP spec settled on <Strong>Streamable HTTP</Strong> as the
|
||||||
|
remote transport (it replaced the older HTTP+SSE pairing), and on{' '}
|
||||||
|
<Strong>OAuth 2.1</Strong> as the auth model. Both are non-negotiable if you want the
|
||||||
|
server installable from Claude Desktop or ChatGPT.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>The OAuth 2.1 pieces you can't skip</H2>
|
||||||
|
<P>
|
||||||
|
MCP authorization is OAuth 2.1, and for remote servers it leans on a few RFCs that older
|
||||||
|
OAuth tutorials don't cover:
|
||||||
|
</P>
|
||||||
|
<UL>
|
||||||
|
<li>
|
||||||
|
<Strong>PKCE (RFC 7636)</Strong> on every authorization-code exchange — mandatory in
|
||||||
|
OAuth 2.1, no exceptions for "confidential" clients.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>Dynamic Client Registration (RFC 7591)</Strong> — clients like Claude Desktop
|
||||||
|
register themselves at runtime; you can't pre-provision a client_id for every user.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>Resource Indicators (RFC 8707)</Strong> — the token has to be bound to the
|
||||||
|
specific MCP server (the <code>resource</code>), so a token minted for one server can't
|
||||||
|
be replayed against another.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>Protected-resource metadata</Strong> — your server returns a{' '}
|
||||||
|
<code>WWW-Authenticate</code> header pointing at the authorization server so clients can
|
||||||
|
discover where to get a token.
|
||||||
|
</li>
|
||||||
|
</UL>
|
||||||
|
<P>
|
||||||
|
Get any of these wrong and the symptom is the same: the client either can't complete the
|
||||||
|
handshake, or it silently fails to discover your auth server. This is the single most
|
||||||
|
common reason a "working" MCP server won't install from Claude.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>Option A — roll your own on generic infra</H2>
|
||||||
|
<P>
|
||||||
|
You can deploy a remote MCP server to <Strong>Cloudflare Workers</Strong> (the most common
|
||||||
|
production choice, edge-global), or to <Strong>Render, Fly, or Cloud Run</Strong> as a
|
||||||
|
normal container. Cloudflare even ships an OAuth provider library for Workers-based MCP
|
||||||
|
servers. This path gives you full control and is the right call if you have engineers and
|
||||||
|
want to own the runtime.
|
||||||
|
</P>
|
||||||
|
<P>
|
||||||
|
The cost is everything around the code: standing up the authorization server (or wiring a
|
||||||
|
third-party IdP correctly for the RFCs above), per-tenant secret storage, TLS, rate
|
||||||
|
limiting, and keeping the transport spec-current as MCP evolves. Budget days, not hours,
|
||||||
|
for the auth layer alone.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>Option B — a platform that wraps it for you</H2>
|
||||||
|
<P>
|
||||||
|
If you already have a server, tools like <Strong>MintMCP</Strong> take a local STDIO
|
||||||
|
server and expose it as a remote one with OAuth wrapping. If you{' '}
|
||||||
|
<Strong>don't have a server yet</Strong>, that's where BuildMyMCPServer fits: you describe
|
||||||
|
the tool in plain language, it generates the TypeScript MCP server, runs static checks,
|
||||||
|
builds a container, and deploys it behind a full OAuth 2.1 authorization server — PKCE,
|
||||||
|
DCR and Resource Indicators included — with copy-paste install snippets for each client.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>A practical checklist before you ship</H2>
|
||||||
|
<UL>
|
||||||
|
<li>Transport is Streamable HTTP, served over TLS at a stable public URL.</li>
|
||||||
|
<li>Unauthenticated request returns 401 + a <code>WWW-Authenticate</code> pointing at your AS.</li>
|
||||||
|
<li>Authorization code flow enforces PKCE (S256), exact redirect-URI match, single-use codes.</li>
|
||||||
|
<li>Issued access tokens are audience-bound to the specific server (RFC 8707).</li>
|
||||||
|
<li>Per-caller secrets are encrypted at rest and injected only at runtime, never logged.</li>
|
||||||
|
<li>You've actually installed it from Claude Desktop end-to-end — not just curl'd it.</li>
|
||||||
|
</UL>
|
||||||
|
|
||||||
|
<P>
|
||||||
|
Whichever route you take, test the real install path in a real client early. See the{' '}
|
||||||
|
<Link href="/guides/hosted-mcp-platforms-compared" className="text-[--color-accent] hover:underline">
|
||||||
|
platform comparison
|
||||||
|
</Link>{' '}
|
||||||
|
for which option fits your situation.
|
||||||
|
</P>
|
||||||
|
</ArticleShell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
import { articleJsonLd, pageMetadata } from '@/lib/seo';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
|
||||||
|
|
||||||
|
const PATH = '/guides/hosted-mcp-platforms-compared';
|
||||||
|
const TITLE =
|
||||||
|
'Hosted MCP platforms compared: Cloudflare, Smithery, Composio & generating your own';
|
||||||
|
const DESCRIPTION =
|
||||||
|
'The MCP hosting landscape splits into four categories — registries, connector platforms, hosting infra, and generators. Here is which one fits which job.';
|
||||||
|
|
||||||
|
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonLd
|
||||||
|
data={articleJsonLd({
|
||||||
|
title: TITLE,
|
||||||
|
description: DESCRIPTION,
|
||||||
|
path: PATH,
|
||||||
|
datePublished: '2026-05-31',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<ArticleShell
|
||||||
|
title={TITLE}
|
||||||
|
subtitle="There are 14,000+ MCP servers out there and a dozen platforms claiming to host them. They are not competing for the same job. Sort them into four buckets and the choice gets obvious."
|
||||||
|
updated="May 2026"
|
||||||
|
>
|
||||||
|
<H2>1. Registries & directories</H2>
|
||||||
|
<P>
|
||||||
|
<Strong>Smithery</Strong>, <Strong>Glama</Strong> and <Strong>PulseMCP</Strong> are about{' '}
|
||||||
|
<em>discovery</em> — finding and listing existing servers (Glama indexes thousands). Some
|
||||||
|
add light hosting on top, but the core value is the catalog and the traffic. Use them to
|
||||||
|
publish a server people can find, or to find one that already does what you need.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>2. Connector platforms</H2>
|
||||||
|
<P>
|
||||||
|
<Strong>Composio</Strong>, <Strong>Nango</Strong>, <Strong>Klavis</Strong>,{' '}
|
||||||
|
<Strong>Zapier</Strong> and <Strong>Pipedream</Strong> expose <em>their</em> catalog of
|
||||||
|
hundreds of pre-built SaaS integrations as MCP, with managed auth. If your need is
|
||||||
|
"let my agent touch Gmail / Slack / Salesforce," these are the fastest path —
|
||||||
|
you're buying breadth of pre-built connectors, not building your own logic.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>3. Hosting infrastructure</H2>
|
||||||
|
<P>
|
||||||
|
<Strong>Cloudflare Workers</Strong> is the default for hosting a remote MCP server in
|
||||||
|
production — edge-global, with an OAuth provider library. <Strong>Vercel</Strong>,{' '}
|
||||||
|
<Strong>Render</Strong> and <Strong>Cloud Run</Strong> host custom Node containers too.{' '}
|
||||||
|
<Strong>MintMCP</Strong> sits slightly higher up: one-click wrap of an existing STDIO
|
||||||
|
server into a remote one with auto-OAuth, and it leads on compliance (SOC 2 Type II,
|
||||||
|
GDPR/HIPAA-formatted audit logs). All of these assume <Strong>you bring the code.</Strong>
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>4. Generators (the gap most lists miss)</H2>
|
||||||
|
<P>
|
||||||
|
The first three categories all assume you already have a server, or that a pre-built
|
||||||
|
connector covers your case. Neither is true when you need a <em>bespoke</em> tool — a
|
||||||
|
wrapper around your own internal API, a niche workflow, a one-off integration nobody has
|
||||||
|
built. That's the generator category: describe the tool, get a custom MCP server hosted
|
||||||
|
for you. It's the youngest and least crowded slice, and it's where{' '}
|
||||||
|
<Strong>BuildMyMCPServer</Strong> plays.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>So which do you pick?</H2>
|
||||||
|
<UL>
|
||||||
|
<li>
|
||||||
|
<Strong>Need a popular SaaS connector?</Strong> A connector platform (Composio / Klavis /
|
||||||
|
Zapier) — don't rebuild what they maintain.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>Have a server and engineers?</Strong> Host it on Cloudflare Workers; wrap your
|
||||||
|
own OAuth or use MintMCP if you want the compliance posture done for you.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>Just browsing for something that exists?</Strong> Smithery or Glama.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>Need a custom tool and don't want to write or host a server?</Strong> A generator
|
||||||
|
— describe it, ship it. Export the source later if you outgrow it.
|
||||||
|
</li>
|
||||||
|
</UL>
|
||||||
|
|
||||||
|
<H2>Where BuildMyMCPServer fits honestly</H2>
|
||||||
|
<P>
|
||||||
|
We're not trying to out-scale Cloudflare's edge or out-catalog Composio. The job we do is
|
||||||
|
the bespoke one: <Strong>prompt → a hosted, OAuth-protected MCP server</Strong>, with
|
||||||
|
install snippets for Claude, Cursor and ChatGPT, an EU/US data-residency choice for teams
|
||||||
|
that care, full TypeScript source export, and a template marketplace to fork from. If your
|
||||||
|
tool is custom and your time is the constraint, that's the wedge. If you need a vetted
|
||||||
|
enterprise SOC 2 host for an existing server today, MintMCP or your own Cloudflare setup is
|
||||||
|
the more honest answer.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<P>
|
||||||
|
Next:{' '}
|
||||||
|
<Link
|
||||||
|
href="/guides/host-mcp-server-with-oauth"
|
||||||
|
className="text-[--color-accent] hover:underline"
|
||||||
|
>
|
||||||
|
what hosting a remote MCP server with OAuth actually involves
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</P>
|
||||||
|
</ArticleShell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
apps/web/app/(marketing)/guides/mintmcp-alternative/page.tsx
Normal file
96
apps/web/app/(marketing)/guides/mintmcp-alternative/page.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
import { articleJsonLd, pageMetadata } from '@/lib/seo';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
|
||||||
|
|
||||||
|
const PATH = '/guides/mintmcp-alternative';
|
||||||
|
const TITLE = 'MintMCP alternative: generate and host a custom MCP server';
|
||||||
|
const DESCRIPTION =
|
||||||
|
'MintMCP wraps an existing STDIO server into a remote one with OAuth. If you do not have a server yet, here is the generate-from-a-prompt alternative — and where MintMCP still wins.';
|
||||||
|
|
||||||
|
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JsonLd
|
||||||
|
data={articleJsonLd({
|
||||||
|
title: TITLE,
|
||||||
|
description: DESCRIPTION,
|
||||||
|
path: PATH,
|
||||||
|
datePublished: '2026-05-31',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<ArticleShell
|
||||||
|
title={TITLE}
|
||||||
|
subtitle="MintMCP and BuildMyMCPServer both get you to a hosted, OAuth-protected MCP server — but they start from opposite ends. The right pick depends entirely on whether you already have server code."
|
||||||
|
updated="May 2026"
|
||||||
|
>
|
||||||
|
<H2>What MintMCP does well</H2>
|
||||||
|
<P>
|
||||||
|
MintMCP takes a local <Strong>STDIO-based MCP server you already wrote</Strong> and turns
|
||||||
|
it into a production remote deployment — one-click, with automatic OAuth wrapping. Its
|
||||||
|
headline strength is <Strong>compliance</Strong>: SOC 2 Type II, with audit logs in SOC 2,
|
||||||
|
HIPAA and GDPR-friendly formats. For an enterprise that already has a server and needs the
|
||||||
|
certifications signed off, that's a strong, honest fit.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>Where it leaves a gap</H2>
|
||||||
|
<P>
|
||||||
|
The model assumes the hard part — designing and writing the server — is already done. If
|
||||||
|
you're starting from <em>"I need a tool that does X"</em> and there's no code
|
||||||
|
yet, a wrapper doesn't help. You still have to learn the MCP SDK, write and test the tool
|
||||||
|
logic, then bring it over.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>The alternative: start from the prompt</H2>
|
||||||
|
<P>
|
||||||
|
<Strong>BuildMyMCPServer</Strong> covers the step before the wrap. You describe the tool in
|
||||||
|
plain language; it generates the TypeScript MCP server, runs static checks against banned
|
||||||
|
patterns, builds a container, and deploys it behind a full OAuth 2.1 authorization server
|
||||||
|
(PKCE, Dynamic Client Registration, Resource Indicators). You get copy-paste install
|
||||||
|
snippets for Claude Desktop, Cursor and ChatGPT, and the full source to export whenever you
|
||||||
|
want.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<H2>Pick by your starting point</H2>
|
||||||
|
<UL>
|
||||||
|
<li>
|
||||||
|
<Strong>You have a working STDIO server + need SOC 2/HIPAA today</Strong> → MintMCP is
|
||||||
|
the more honest fit. We don't claim those certifications.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>You have an idea, not a server</Strong> → generate it here, ship in minutes, and
|
||||||
|
export the TypeScript if you later move it onto your own infra.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>You're an agency building one-off tools for clients repeatedly</Strong> →
|
||||||
|
generation + a fork-able template marketplace removes the per-client boilerplate.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Strong>You're in the EU/DACH and care where prompts go</Strong> → we expose the provider
|
||||||
|
and offer a data-residency choice rather than defaulting everything to one region.
|
||||||
|
</li>
|
||||||
|
</UL>
|
||||||
|
|
||||||
|
<H2>What's the same either way</H2>
|
||||||
|
<P>
|
||||||
|
Both deliver a remote, OAuth-protected MCP server at a stable URL that real clients can
|
||||||
|
install — neither leaves you hand-rolling the auth handshake. The difference is purely{' '}
|
||||||
|
<Strong>where you start</Strong>: with code, or with a sentence.
|
||||||
|
</P>
|
||||||
|
|
||||||
|
<P>
|
||||||
|
More on the landscape:{' '}
|
||||||
|
<Link
|
||||||
|
href="/guides/hosted-mcp-platforms-compared"
|
||||||
|
className="text-[--color-accent] hover:underline"
|
||||||
|
>
|
||||||
|
hosted MCP platforms compared
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</P>
|
||||||
|
</ArticleShell>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
apps/web/app/(marketing)/guides/page.tsx
Normal file
64
apps/web/app/(marketing)/guides/page.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { pageMetadata } from '@/lib/seo';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export const metadata = pageMetadata({
|
||||||
|
title: 'MCP guides',
|
||||||
|
description:
|
||||||
|
'Practical guides on hosting, securing and shipping Model Context Protocol (MCP) servers — OAuth 2.1, remote transport, platform comparisons.',
|
||||||
|
path: '/guides',
|
||||||
|
});
|
||||||
|
|
||||||
|
const GUIDES = [
|
||||||
|
{
|
||||||
|
slug: 'host-mcp-server-with-oauth',
|
||||||
|
title: 'How to host a remote MCP server with OAuth (2026)',
|
||||||
|
description:
|
||||||
|
'Streamable HTTP, OAuth 2.1, PKCE and Resource Indicators — what it actually takes to put a remote MCP server in production, and the shortcuts.',
|
||||||
|
tag: 'Guide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'hosted-mcp-platforms-compared',
|
||||||
|
title: 'Hosted MCP platforms compared: Cloudflare, Smithery, Composio & generating your own',
|
||||||
|
description:
|
||||||
|
'The MCP hosting landscape splits into four categories. Which one fits depends on whether you have a server already, need a catalog, or need bespoke logic.',
|
||||||
|
tag: 'Comparison',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'mintmcp-alternative',
|
||||||
|
title: 'MintMCP alternative: generate and host a custom MCP server',
|
||||||
|
description:
|
||||||
|
'MintMCP wraps an existing STDIO server into a remote one. If you do not have a server yet, here is the generate-from-a-prompt route — and where MintMCP still wins.',
|
||||||
|
tag: 'Alternative',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function GuidesIndex() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-3xl px-6 py-14">
|
||||||
|
<h1 className="text-[28px] font-semibold tracking-tight text-[--color-fg]">MCP guides</h1>
|
||||||
|
<p className="mt-2 text-[14.5px] leading-relaxed text-[--color-fg-muted]">
|
||||||
|
Hosting, auth and shipping for Model Context Protocol servers — written for people building
|
||||||
|
real tools, not demos.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 space-y-3">
|
||||||
|
{GUIDES.map((g) => (
|
||||||
|
<Link
|
||||||
|
key={g.slug}
|
||||||
|
href={`/guides/${g.slug}`}
|
||||||
|
className="block rounded-lg border border-[--color-border] p-4 transition-colors hover:bg-[--color-bg-subtle]"
|
||||||
|
>
|
||||||
|
<span className="mono text-[10.5px] uppercase tracking-wider text-[--color-fg-subtle]">
|
||||||
|
{g.tag}
|
||||||
|
</span>
|
||||||
|
<h2 className="mt-1 text-[16px] font-semibold tracking-tight text-[--color-fg]">
|
||||||
|
{g.title}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1.5 text-[13px] leading-relaxed text-[--color-fg-muted]">
|
||||||
|
{g.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,6 +24,9 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
|
|||||||
<Link href="/docs" className="transition-colors hover:text-[--color-fg]">
|
<Link href="/docs" className="transition-colors hover:text-[--color-fg]">
|
||||||
Docs
|
Docs
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/guides" className="transition-colors hover:text-[--color-fg]">
|
||||||
|
Guides
|
||||||
|
</Link>
|
||||||
<Link href="/changelog" className="transition-colors hover:text-[--color-fg]">
|
<Link href="/changelog" className="transition-colors hover:text-[--color-fg]">
|
||||||
Changelog
|
Changelog
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { SITE_URL } from '@/lib/seo';
|
import { SITE_URL } from '@/lib/seo';
|
||||||
|
import { fetchPublicTemplateSlugs } from '@/lib/templates-server';
|
||||||
import type { MetadataRoute } from 'next';
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
type Entry = {
|
type Entry = {
|
||||||
@ -11,6 +12,10 @@ const ROUTES: Entry[] = [
|
|||||||
{ path: '/', priority: 1.0, changeFrequency: 'weekly' },
|
{ path: '/', priority: 1.0, changeFrequency: 'weekly' },
|
||||||
{ path: '/pricing', priority: 0.9, changeFrequency: 'weekly' },
|
{ path: '/pricing', priority: 0.9, changeFrequency: 'weekly' },
|
||||||
{ path: '/templates', priority: 0.9, changeFrequency: 'daily' },
|
{ path: '/templates', priority: 0.9, changeFrequency: 'daily' },
|
||||||
|
{ path: '/guides', priority: 0.8, changeFrequency: 'weekly' },
|
||||||
|
{ path: '/guides/host-mcp-server-with-oauth', priority: 0.8, changeFrequency: 'monthly' },
|
||||||
|
{ path: '/guides/hosted-mcp-platforms-compared', priority: 0.8, changeFrequency: 'monthly' },
|
||||||
|
{ path: '/guides/mintmcp-alternative', priority: 0.7, changeFrequency: 'monthly' },
|
||||||
{ path: '/docs', priority: 0.8, changeFrequency: 'weekly' },
|
{ path: '/docs', priority: 0.8, changeFrequency: 'weekly' },
|
||||||
{ path: '/docs/concepts', priority: 0.7, changeFrequency: 'monthly' },
|
{ path: '/docs/concepts', priority: 0.7, changeFrequency: 'monthly' },
|
||||||
{ path: '/docs/oauth', priority: 0.7, changeFrequency: 'monthly' },
|
{ path: '/docs/oauth', priority: 0.7, changeFrequency: 'monthly' },
|
||||||
@ -25,12 +30,24 @@ const ROUTES: Entry[] = [
|
|||||||
{ path: '/terms', priority: 0.3, changeFrequency: 'yearly' },
|
{ path: '/terms', priority: 0.3, changeFrequency: 'yearly' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return ROUTES.map((r) => ({
|
const staticEntries: MetadataRoute.Sitemap = ROUTES.map((r) => ({
|
||||||
url: `${SITE_URL}${r.path}`,
|
url: `${SITE_URL}${r.path}`,
|
||||||
lastModified: now,
|
lastModified: now,
|
||||||
changeFrequency: r.changeFrequency,
|
changeFrequency: r.changeFrequency,
|
||||||
priority: r.priority,
|
priority: r.priority,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Marketplace templates — each public template is its own indexable page.
|
||||||
|
// Best-effort: if the API is unreachable the static entries still ship.
|
||||||
|
const slugs = await fetchPublicTemplateSlugs();
|
||||||
|
const templateEntries: MetadataRoute.Sitemap = slugs.map((slug) => ({
|
||||||
|
url: `${SITE_URL}/templates/${slug}`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.6,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...staticEntries, ...templateEntries];
|
||||||
}
|
}
|
||||||
|
|||||||
34
apps/web/app/templates/[slug]/collapsible-code.tsx
Normal file
34
apps/web/app/templates/[slug]/collapsible-code.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CodeBlock } from '@/components/code-block';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client island: the "audit the generated code before you fork" toggle. Kept
|
||||||
|
* out of the server-rendered page so the page itself stays static + indexable.
|
||||||
|
*/
|
||||||
|
export function CollapsibleCode({ code }: { code: string }) {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
return (
|
||||||
|
<section className="mt-10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShow((s) => !s)}
|
||||||
|
className="inline-flex items-center gap-1 text-[14px] font-semibold tracking-tight text-[--color-fg] transition-colors hover:text-[--color-fg-muted]"
|
||||||
|
>
|
||||||
|
{show ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
Generated code ({code.length} chars)
|
||||||
|
</button>
|
||||||
|
<p className="mt-1 text-[12px] text-[--color-fg-muted]">
|
||||||
|
Audit before you fork. We re-scan every published template for banned patterns (eval,
|
||||||
|
child_process, prompt-injection markers).
|
||||||
|
</p>
|
||||||
|
{show && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<CodeBlock label="src/server.ts" code={code} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,95 +1,71 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
|
||||||
import { ShieldCheck, GitFork, Activity, ExternalLink, ChevronDown, ChevronRight } from 'lucide-react';
|
|
||||||
import { apiFetch } from '@/lib/api';
|
|
||||||
import { Logo } from '@/components/logo';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { CodeBlock } from '@/components/code-block';
|
import { CodeBlock } from '@/components/code-block';
|
||||||
|
import { JsonLd } from '@/components/json-ld';
|
||||||
|
import { Logo } from '@/components/logo';
|
||||||
|
import { type TemplateDetail, fetchTemplate } from '@/lib/templates-server';
|
||||||
|
import { pageMetadata, templateJsonLd } from '@/lib/seo';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { Activity, ExternalLink, GitFork, ShieldCheck } from 'lucide-react';
|
||||||
|
import { CollapsibleCode } from './collapsible-code';
|
||||||
|
|
||||||
interface Tool {
|
// Server-rendered for SEO: per-template <title>, description, OpenGraph and
|
||||||
name: string;
|
// SoftwareApplication JSON-LD. The only interactive piece (the code-audit
|
||||||
description: string;
|
// toggle) lives in a client island.
|
||||||
inputSchema: Record<string, unknown>;
|
|
||||||
|
interface PageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SecretHint {
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
key: string;
|
const { slug } = await params;
|
||||||
description: string;
|
const t = await fetchTemplate(slug);
|
||||||
howToGetUrl?: string;
|
if (!t) {
|
||||||
}
|
return pageMetadata({
|
||||||
|
title: 'Template not found',
|
||||||
interface TemplateDetail {
|
description: 'This MCP server template is not available.',
|
||||||
id: string;
|
path: `/templates/${slug}`,
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
shortDescription: string;
|
|
||||||
longDescription: string | null;
|
|
||||||
category: string;
|
|
||||||
status: 'draft' | 'public' | 'hidden' | 'takedown';
|
|
||||||
verified: boolean;
|
|
||||||
forkCount: number;
|
|
||||||
activeDeployments: number;
|
|
||||||
toolsSchema: Tool[];
|
|
||||||
generatedCode: string;
|
|
||||||
requiredSecrets: SecretHint[];
|
|
||||||
scopes: string[];
|
|
||||||
ownerName: string | null;
|
|
||||||
ownerOrgName: string | null;
|
|
||||||
sourceServerId: string | null;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TemplateDetail() {
|
|
||||||
const params = useParams<{ slug: string }>();
|
|
||||||
const router = useRouter();
|
|
||||||
const [template, setTemplate] = useState<TemplateDetail | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [showCode, setShowCode] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
apiFetch<{ template: TemplateDetail }>(`/v1/templates/${params.slug}`)
|
|
||||||
.then((r) => setTemplate(r.template))
|
|
||||||
.catch((e) => {
|
|
||||||
const detail = (e as { detail?: { error?: string } }).detail;
|
|
||||||
setError(detail?.error ?? (e as Error).message);
|
|
||||||
});
|
});
|
||||||
}, [params.slug]);
|
|
||||||
|
|
||||||
function useTemplate() {
|
|
||||||
if (!template) return;
|
|
||||||
router.push(`/servers/new?template=${template.slug}`);
|
|
||||||
}
|
}
|
||||||
|
return pageMetadata({
|
||||||
|
title: `${t.title} — MCP server for Claude, Cursor & ChatGPT`,
|
||||||
|
description:
|
||||||
|
t.shortDescription.length > 0
|
||||||
|
? t.shortDescription
|
||||||
|
: `Fork the ${t.title} MCP server and deploy your own OAuth-protected copy in seconds.`,
|
||||||
|
path: `/templates/${slug}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
export default async function TemplateDetailPage({ params }: PageProps) {
|
||||||
return (
|
const { slug } = await params;
|
||||||
<div className="flex min-h-screen items-center justify-center px-6">
|
const template: TemplateDetail | null = await fetchTemplate(slug);
|
||||||
<div className="text-center">
|
if (!template) notFound();
|
||||||
<p className="text-[14px]">Template not found.</p>
|
|
||||||
<Link href="/templates" className="mt-3 inline-block text-[12px] text-[--color-accent] underline">
|
|
||||||
← Back to marketplace
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!template) {
|
const forkHref = `/servers/new?template=${template.slug}`;
|
||||||
return (
|
|
||||||
<div className="px-8 py-20 text-center mono text-[12px] text-[--color-fg-muted]">Loading…</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<JsonLd
|
||||||
|
data={templateJsonLd({
|
||||||
|
slug: template.slug,
|
||||||
|
title: template.title,
|
||||||
|
description: template.shortDescription,
|
||||||
|
category: template.category,
|
||||||
|
tools: template.toolsSchema.map((t) => t.name),
|
||||||
|
author: template.ownerName ?? template.ownerOrgName,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/85 backdrop-blur-md">
|
<header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/85 backdrop-blur-md">
|
||||||
<div className="mx-auto flex h-12 max-w-5xl items-center justify-between px-6">
|
<div className="mx-auto flex h-12 max-w-5xl items-center justify-between px-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Logo />
|
<Logo />
|
||||||
<span className="text-[12.5px] text-[--color-fg-subtle]">
|
<span className="text-[12.5px] text-[--color-fg-subtle]">
|
||||||
/ <Link href="/templates" className="hover:text-[--color-fg]">templates</Link> / {template.slug}
|
/{' '}
|
||||||
|
<Link href="/templates" className="hover:text-[--color-fg]">
|
||||||
|
templates
|
||||||
|
</Link>{' '}
|
||||||
|
/ {template.slug}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
@ -130,17 +106,20 @@ export default function TemplateDetail() {
|
|||||||
Tools ({template.toolsSchema.length})
|
Tools ({template.toolsSchema.length})
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-3">
|
||||||
{template.toolsSchema.map((tool) => (
|
{template.toolsSchema.map((tool) => {
|
||||||
|
const paramCount = Object.keys(tool.inputSchema ?? {}).length;
|
||||||
|
return (
|
||||||
<div key={tool.name} className="panel p-3">
|
<div key={tool.name} className="panel p-3">
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex items-baseline gap-2">
|
||||||
<span className="mono text-[13px] font-semibold">{tool.name}</span>
|
<span className="mono text-[13px] font-semibold">{tool.name}</span>
|
||||||
<span className="mono text-[10.5px] text-[--color-fg-subtle]">
|
<span className="mono text-[10.5px] text-[--color-fg-subtle]">
|
||||||
{Object.keys(tool.inputSchema ?? {}).length} param
|
{paramCount} param{paramCount === 1 ? '' : 's'}
|
||||||
{Object.keys(tool.inputSchema ?? {}).length === 1 ? '' : 's'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-[12.5px] text-[--color-fg-muted]">{tool.description}</p>
|
<p className="mt-1.5 text-[12.5px] text-[--color-fg-muted]">
|
||||||
{Object.keys(tool.inputSchema ?? {}).length > 0 && (
|
{tool.description}
|
||||||
|
</p>
|
||||||
|
{paramCount > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
label="input schema"
|
label="input schema"
|
||||||
@ -149,7 +128,8 @@ export default function TemplateDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -178,62 +158,34 @@ export default function TemplateDetail() {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-[12.5px] text-[--color-fg-muted]">
|
<p className="mt-1.5 text-[12.5px] text-[--color-fg-muted]">{s.description}</p>
|
||||||
{s.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="mt-10">
|
<CollapsibleCode code={template.generatedCode} />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowCode((s) => !s)}
|
|
||||||
className="inline-flex items-center gap-1 text-[14px] font-semibold tracking-tight text-[--color-fg] transition-colors hover:text-[--color-fg-muted]"
|
|
||||||
>
|
|
||||||
{showCode ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
||||||
Generated code ({template.generatedCode.length} chars)
|
|
||||||
</button>
|
|
||||||
<p className="mt-1 text-[12px] text-[--color-fg-muted]">
|
|
||||||
Audit before you fork. We re-scan every published template for banned patterns
|
|
||||||
(eval, child_process, prompt-injection markers).
|
|
||||||
</p>
|
|
||||||
{showCode && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<CodeBlock label="src/server.ts" code={template.generatedCode} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="space-y-3">
|
<aside className="space-y-3">
|
||||||
<div className="panel p-4">
|
<div className="panel p-4">
|
||||||
{template.status === 'public' ? (
|
{template.status === 'public' ? (
|
||||||
<>
|
<>
|
||||||
<Button variant="primary" size="lg" className="w-full" onClick={useTemplate}>
|
<Link
|
||||||
|
href={forkHref}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-center rounded-md bg-[--color-accent] text-[13.5px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
|
||||||
|
>
|
||||||
Fork this template →
|
Fork this template →
|
||||||
</Button>
|
</Link>
|
||||||
<p className="mt-2 text-[11.5px] text-[--color-fg-muted]">
|
<p className="mt-2 text-[11.5px] text-[--color-fg-muted]">
|
||||||
One click → your own isolated container.
|
One click → your own isolated container.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
|
||||||
<div className="rounded-md border border-amber-400/30 bg-amber-400/5 p-2.5 text-[12px] text-amber-200/90">
|
<div className="rounded-md border border-amber-400/30 bg-amber-400/5 p-2.5 text-[12px] text-amber-200/90">
|
||||||
This template is <span className="mono">{template.status}</span> — not
|
This template is <span className="mono">{template.status}</span> — not forkable.
|
||||||
forkable. {template.sourceServerId ? 'Re-share it from the server’s Publish tab to allow forks.' : ''}
|
|
||||||
</div>
|
</div>
|
||||||
{template.sourceServerId && (
|
|
||||||
<a
|
|
||||||
href={`/servers/${template.sourceServerId}`}
|
|
||||||
className="mt-2 inline-flex h-8 w-full items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] text-[12.5px] text-[--color-fg] transition-colors hover:bg-[--color-bg-subtle]"
|
|
||||||
>
|
|
||||||
Manage in server →
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -245,17 +197,17 @@ export default function TemplateDetail() {
|
|||||||
icon={<Activity size={11} />}
|
icon={<Activity size={11} />}
|
||||||
/>
|
/>
|
||||||
<Row label="Category" value={template.category} mono />
|
<Row label="Category" value={template.category} mono />
|
||||||
|
<Row label="Published" value={new Date(template.createdAt).toLocaleDateString()} />
|
||||||
<Row
|
<Row
|
||||||
label="Published"
|
label="Author"
|
||||||
value={new Date(template.createdAt).toLocaleDateString()}
|
value={template.ownerName ?? template.ownerOrgName ?? 'anonymous'}
|
||||||
/>
|
/>
|
||||||
<Row label="Author" value={template.ownerName ?? template.ownerOrgName ?? 'anonymous'} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel p-3 text-[11.5px] leading-relaxed text-[--color-fg-muted]">
|
<div className="panel p-3 text-[11.5px] leading-relaxed text-[--color-fg-muted]">
|
||||||
<strong className="text-[--color-fg]">Forking is safe.</strong> Your fork gets its own
|
<strong className="text-[--color-fg]">Forking is safe.</strong> Your fork gets its own
|
||||||
Docker container, its own port, its own AES-256-encrypted secrets. The template
|
Docker container, its own port, its own AES-256-encrypted secrets. The template author
|
||||||
author has no visibility into your traffic or data.
|
has no visibility into your traffic or data.
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -184,6 +184,58 @@ export function faqJsonLd(items: FaqItem[] = FAQ): object {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** TechArticle structured data for /guides/* SEO articles. */
|
||||||
|
export function articleJsonLd(opts: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
path: string;
|
||||||
|
datePublished: string;
|
||||||
|
dateModified?: string;
|
||||||
|
}): object {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'TechArticle',
|
||||||
|
headline: opts.title,
|
||||||
|
description: opts.description,
|
||||||
|
url: `${SITE_URL}${opts.path}`,
|
||||||
|
mainEntityOfPage: { '@type': 'WebPage', '@id': `${SITE_URL}${opts.path}` },
|
||||||
|
datePublished: opts.datePublished,
|
||||||
|
dateModified: opts.dateModified ?? opts.datePublished,
|
||||||
|
inLanguage: 'en',
|
||||||
|
author: { '@id': `${SITE_URL}/#organization` },
|
||||||
|
publisher: { '@id': `${SITE_URL}/#organization` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SoftwareApplication structured data for a published marketplace template. */
|
||||||
|
export function templateJsonLd(opts: {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
tools: string[];
|
||||||
|
author: string | null;
|
||||||
|
}): object {
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'SoftwareApplication',
|
||||||
|
'@id': `${SITE_URL}/templates/${opts.slug}#software`,
|
||||||
|
name: opts.title,
|
||||||
|
description: opts.description,
|
||||||
|
url: `${SITE_URL}/templates/${opts.slug}`,
|
||||||
|
applicationCategory: 'DeveloperApplication',
|
||||||
|
applicationSubCategory: 'MCP server',
|
||||||
|
operatingSystem: 'Web Browser',
|
||||||
|
inLanguage: 'en',
|
||||||
|
keywords: ['MCP server', 'Model Context Protocol', opts.category],
|
||||||
|
featureList: opts.tools,
|
||||||
|
isAccessibleForFree: true,
|
||||||
|
offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
|
||||||
|
...(opts.author ? { author: { '@type': 'Person', name: opts.author } } : {}),
|
||||||
|
publisher: { '@id': `${SITE_URL}/#organization` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-page metadata. `title` is a bare string so the root layout's
|
* Per-page metadata. `title` is a bare string so the root layout's
|
||||||
* "%s | BuildMyMCPServer" template appends the brand exactly once.
|
* "%s | BuildMyMCPServer" template appends the brand exactly once.
|
||||||
|
|||||||
76
apps/web/lib/templates-server.ts
Normal file
76
apps/web/lib/templates-server.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// Server-only fetchers for the public template marketplace. Used by the
|
||||||
|
// server-rendered template detail page (SEO metadata + JSON-LD) and the
|
||||||
|
// sitemap. Never import this into a client component.
|
||||||
|
|
||||||
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000';
|
||||||
|
|
||||||
|
export interface TemplateTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateSecretHint {
|
||||||
|
key: string;
|
||||||
|
description: string;
|
||||||
|
howToGetUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateDetail {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
shortDescription: string;
|
||||||
|
longDescription: string | null;
|
||||||
|
category: string;
|
||||||
|
status: 'draft' | 'public' | 'hidden' | 'takedown';
|
||||||
|
verified: boolean;
|
||||||
|
forkCount: number;
|
||||||
|
activeDeployments: number;
|
||||||
|
toolsSchema: TemplateTool[];
|
||||||
|
generatedCode: string;
|
||||||
|
requiredSecrets: TemplateSecretHint[];
|
||||||
|
scopes: string[];
|
||||||
|
ownerName: string | null;
|
||||||
|
ownerOrgName: string | null;
|
||||||
|
sourceServerId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single public template by slug for server rendering. Returns null
|
||||||
|
* for missing / non-public templates so the page can `notFound()` — we only
|
||||||
|
* want `public` templates indexed.
|
||||||
|
*/
|
||||||
|
export async function fetchTemplate(slug: string): Promise<TemplateDetail | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/v1/templates/${encodeURIComponent(slug)}`, {
|
||||||
|
// Cache server-side for 5 min so crawler hits don't hammer the API.
|
||||||
|
next: { revalidate: 300 },
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = (await res.json()) as { template?: TemplateDetail };
|
||||||
|
const t = data.template;
|
||||||
|
if (!t || t.status !== 'public') return null;
|
||||||
|
return t;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slugs of public templates, for the sitemap. Best-effort: returns [] on error. */
|
||||||
|
export async function fetchPublicTemplateSlugs(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/v1/templates?limit=100&sort=newest`, {
|
||||||
|
next: { revalidate: 600 },
|
||||||
|
});
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const data = (await res.json()) as
|
||||||
|
| { templates?: Array<{ slug?: string }> }
|
||||||
|
| Array<{ slug?: string }>;
|
||||||
|
const list = Array.isArray(data) ? data : (data.templates ?? []);
|
||||||
|
return list.map((t) => t.slug).filter((s): s is string => typeof s === 'string');
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user