- /docs subpages canonicalized to /docs and had no descriptions; each now uses pageMetadata with its own path (was: Google could index at most one) - llms.txt claimed Team EUR 149, RBAC/SLA and BYO-cloud that do not exist; regenerated from lib/seo.ts truth + new llms-full.txt with docs content - sitemap stamped lastModified=now on every request; now real per-route dates from new lib/articles.ts registry (single source for guides index/sitemap/RSS) - new /feed.xml RSS 2.0 route + alternates link in root layout - articleJsonLd: image (per-guide opengraph-image via lib/og-article.tsx), Person author, wordCount; new breadcrumbJsonLd on guides + docs - GSC verification via NEXT_PUBLIC_GSC_VERIFICATION (documented in .env.example) - dropped fabricated Enterprise EUR 499 offer from SoftwareApplication JSON-LD - article-shell: OL/Table/Note primitives for upcoming articles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
import {
|
|
DocsTitle,
|
|
DocsLead,
|
|
DocsH2,
|
|
DocsP,
|
|
DocsList,
|
|
DocsLi,
|
|
DocsCode,
|
|
Mono,
|
|
} from '@/components/docs-page';
|
|
import { JsonLd } from '@/components/json-ld';
|
|
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
|
|
|
|
export const metadata = pageMetadata({
|
|
title: 'Authoring tools',
|
|
description:
|
|
'How to write prompts that generate good MCP tools — naming, input schemas, credentials, and how the generated TypeScript is checked before it ships.',
|
|
path: '/docs/authoring',
|
|
});
|
|
|
|
export default function Authoring() {
|
|
return (
|
|
<>
|
|
<JsonLd
|
|
data={breadcrumbJsonLd([
|
|
{ name: 'Home', path: '/' },
|
|
{ name: 'Docs', path: '/docs' },
|
|
{ name: 'Authoring tools', path: '/docs/authoring' },
|
|
])}
|
|
/>
|
|
<DocsTitle kicker="Build">Authoring tools</DocsTitle>
|
|
<DocsLead>
|
|
What you write in the prompt is what Claude turns into TypeScript. Better prompts mean
|
|
better tools. These patterns cover 80% of the common asks.
|
|
</DocsLead>
|
|
|
|
<DocsH2 id="anatomy">Anatomy of a tool</DocsH2>
|
|
<DocsP>Each generated tool ends up looking like this:</DocsP>
|
|
<DocsCode
|
|
label="generated TypeScript"
|
|
code={`server.registerTool(
|
|
'search_pages',
|
|
{
|
|
title: 'search_pages',
|
|
description: 'Search Notion pages matching a query.',
|
|
inputSchema: {
|
|
query: z.string().describe('search terms'),
|
|
},
|
|
},
|
|
async (args) => {
|
|
try {
|
|
const res = await fetch('https://api.notion.com/v1/search', {
|
|
method: 'POST',
|
|
signal: AbortSignal.timeout(10000),
|
|
headers: {
|
|
'Authorization': \`Bearer \${process.env.NOTION_API_KEY}\`,
|
|
'Notion-Version': '2022-06-28',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ query: args.query }),
|
|
});
|
|
const data = await res.json();
|
|
return { content: [{ type: 'text', text: JSON.stringify(data.results) }] };
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
return { content: [{ type: 'text', text: 'Error: ' + msg }], isError: true };
|
|
}
|
|
},
|
|
);`}
|
|
/>
|
|
|
|
<DocsH2 id="rules">Rules the generator enforces</DocsH2>
|
|
<DocsList>
|
|
<DocsLi>No <Mono>eval</Mono>, no <Mono>new Function</Mono>, no <Mono>child_process</Mono>. The static check rejects the build.</DocsLi>
|
|
<DocsLi>No <Mono>import</Mono> statements in tool bodies — the runtime injects <Mono>fetch</Mono>, <Mono>pg</Mono>, <Mono>z</Mono>.</DocsLi>
|
|
<DocsLi>Secrets live in <Mono>process.env</Mono>. Never embedded literally.</DocsLi>
|
|
<DocsLi>External HTTP calls must use <Mono>AbortSignal.timeout</Mono>. Default 10s.</DocsLi>
|
|
<DocsLi>Database access via <Mono>pg</Mono> with parameterized queries only.</DocsLi>
|
|
<DocsLi>Errors return as MCP error-content, not thrown exceptions.</DocsLi>
|
|
</DocsList>
|
|
|
|
<DocsH2 id="patterns">Prompt patterns that work</DocsH2>
|
|
<DocsP>
|
|
<strong>Be explicit about tool names.</strong> "Tool: <Mono>search_pages(query)</Mono>"
|
|
beats "give me a search tool".
|
|
</DocsP>
|
|
<DocsP>
|
|
<strong>Name the credentials.</strong> "Auth: <Mono>NOTION_API_KEY</Mono>" tells the
|
|
generator what to put in <Mono>requiredSecrets</Mono>. Saves an iteration.
|
|
</DocsP>
|
|
<DocsP>
|
|
<strong>Say if a tool is destructive.</strong> "Tool: <Mono>delete_page(page_id)</Mono> —
|
|
destructive, permanently removes the page" surfaces the warning to the AI client.
|
|
</DocsP>
|
|
<DocsP>
|
|
<strong>One server per integration, not per tool.</strong> A Notion server with five
|
|
tools is cleaner than five Notion servers each with one tool.
|
|
</DocsP>
|
|
|
|
<DocsH2 id="iteration">Iterate on a live server</DocsH2>
|
|
<DocsP>
|
|
Open the server detail page, click the <Mono>Iterate</Mono> tab, describe what you want
|
|
to add. A new build version is queued, rolling-deployed, the old version stays live until
|
|
the new one is healthy.
|
|
</DocsP>
|
|
</>
|
|
);
|
|
}
|