Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
89 lines
3.6 KiB
TypeScript
89 lines
3.6 KiB
TypeScript
import { IRequest } from 'itty-router'
|
|
import { Environment } from '../../types'
|
|
import { MCP_SERVER_INFO } from './boardTools'
|
|
import { getMcpResourceUrl } from './mcpAuth'
|
|
import { SUPPORTED_PROTOCOL_VERSIONS } from './mcpServer'
|
|
|
|
// A Server Card is the pre-connection half of MCP discovery: what a client reads to decide whether
|
|
// it wants this server and how to reach it, without a token. That matters here, where every call to
|
|
// the server itself is refused without one — an agent that cannot authenticate can still learn where
|
|
// to go.
|
|
//
|
|
// Nothing checks the card against the running server, so the values below are sourced from the same
|
|
// constants it answers with rather than restated.
|
|
|
|
/**
|
|
* The path the MCP spec reserves for a card: the server's own URL with `/server-card` appended. As
|
|
* registered on the router — the public form is `/api/app/mcp/server-card`, with the `/api` prefix
|
|
* stripped upstream.
|
|
*/
|
|
export const MCP_SERVER_CARD_PATH = '/app/mcp/server-card'
|
|
|
|
/**
|
|
* A second path for the same document, under `.well-known`.
|
|
*
|
|
* The extension's own discovery notes argue against this: a card is not site-wide the way OAuth
|
|
* metadata is, and the catalog at `/.well-known/ai-catalog.json` is the mechanism that is supposed
|
|
* to point at cards. It is served anyway because agent-readiness scanners probe it directly, and one
|
|
* more route pointing at the same handler costs less than being invisible to them. Prefer
|
|
* {@link MCP_SERVER_CARD_PATH} when linking.
|
|
*
|
|
* At the origin rather than under `/api`, so — like the protected resource metadata — it needs its
|
|
* own `wrangler.toml` route to reach this worker at all.
|
|
*/
|
|
export const MCP_SERVER_CARD_WELL_KNOWN_PATH = '/.well-known/mcp/server-card.json'
|
|
|
|
/** Not a version to bump: a breaking revision of the card shape publishes a new `vN` URL. */
|
|
const SERVER_CARD_SCHEMA_URL =
|
|
'https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json'
|
|
|
|
/**
|
|
* Reverse-DNS with exactly one slash, which the card schema requires and the runtime
|
|
* `serverInfo.name` (`tldraw-boards`) does not satisfy — the two identify the same server in two
|
|
* namespaces that cannot be made textually equal. The human-readable identity travels in `title`
|
|
* and `description`, which do match.
|
|
*/
|
|
const SERVER_CARD_NAME = 'com.tldraw/boards'
|
|
|
|
/**
|
|
* The media type a Server Card is served with, which is not `application/json`.
|
|
*
|
|
* Clients ask for this type by `Accept`, and the AI Catalog entry pointing here declares it — a
|
|
* catalog whose `type` disagrees with what the URL actually returns is the mismatch a strict client
|
|
* refuses on. `Response.json` would quietly make it `application/json`, so the card is serialised by
|
|
* hand.
|
|
*/
|
|
const SERVER_CARD_MEDIA_TYPE = 'application/mcp-server-card+json'
|
|
|
|
export function getMcpServerCard(request: IRequest, env: Environment): Response {
|
|
return new Response(
|
|
JSON.stringify({
|
|
$schema: SERVER_CARD_SCHEMA_URL,
|
|
name: SERVER_CARD_NAME,
|
|
version: MCP_SERVER_INFO.version,
|
|
title: MCP_SERVER_INFO.title,
|
|
// The schema caps this at 100 characters.
|
|
description: 'Search, inspect and screenshot the tldraw.com boards you have access to.',
|
|
websiteUrl: 'https://tldraw.dev',
|
|
repository: {
|
|
url: 'https://github.com/tldraw/tldraw',
|
|
source: 'github',
|
|
subfolder: 'apps/dotcom/sync-worker',
|
|
},
|
|
remotes: [
|
|
{
|
|
type: 'streamable-http',
|
|
url: getMcpResourceUrl(request, env),
|
|
supportedProtocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
|
|
},
|
|
],
|
|
}),
|
|
{
|
|
headers: {
|
|
'content-type': SERVER_CARD_MEDIA_TYPE,
|
|
'cache-control': 'public, max-age=3600',
|
|
},
|
|
}
|
|
)
|
|
}
|