-
- - Open Customize > Connectors in claude.ai or the Claude desktop app.
- - Select Add custom connector. Name it skilld and paste the server URL. If Claude asks about authentication, choose No sign-in.
- - In a chat, open the + menu, select Connectors, and check that skilld is on.
-
+
+
+ Claude opens the Add custom connector dialog with skilld filled in. Check the name and the server URL, then confirm. In a chat, open the + menu, select Connectors, and check that skilld is on.
+
+
+ Or open Customize > Connectors in claude.ai or the Claude desktop app. Select Add custom connector, name it skilld, and paste the server URL.
+
Every plan can add a custom connector. The Free plan allows one. On a Team or Enterprise plan, an Owner adds it for the organization first.
Read Anthropic's guide.
diff --git a/layers/marketing/app/pages/terms.vue b/layers/marketing/app/pages/terms.vue
new file mode 100644
index 00000000..a75dce2e
--- /dev/null
+++ b/layers/marketing/app/pages/terms.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/layers/marketing/app/utils/developer-setup.ts b/layers/marketing/app/utils/developer-setup.ts
index ab48fc42..2a1ef335 100644
--- a/layers/marketing/app/utils/developer-setup.ts
+++ b/layers/marketing/app/utils/developer-setup.ts
@@ -42,6 +42,15 @@ export function cursorInstallUrl(url: string = REGISTRY_MCP_URL): string {
return `cursor://anysphere.cursor-deeplink/mcp/install?name=${SERVER_NAME}&config=${encodeURIComponent(config)}`
}
+/**
+ * Claude's link opens its Add custom connector dialog with the name and URL
+ * filled in. The user still confirms before Claude adds anything.
+ */
+export function claudeConnectorUrl(url: string = REGISTRY_MCP_URL): string {
+ const query = new URLSearchParams({ modal: 'add-custom-connector', connectorName: SERVER_NAME, connectorUrl: url })
+ return `https://claude.ai/customize/connectors?${query}`
+}
+
/** VS Code's install link carries the whole server entry as URI-encoded JSON. */
export function vscodeInstallUrl(url: string = REGISTRY_MCP_URL): string {
return `vscode:mcp/install?${encodeURIComponent(vscodeServerJson(url))}`
diff --git a/layers/marketing/app/utils/page-admissions.ts b/layers/marketing/app/utils/page-admissions.ts
index 7fa1d897..d491b49b 100644
--- a/layers/marketing/app/utils/page-admissions.ts
+++ b/layers/marketing/app/utils/page-admissions.ts
@@ -45,6 +45,7 @@ export const FREEZE_AUDIT_PATHS: readonly string[] = [
'/developers',
'/skillgen',
'/privacy',
+ '/terms',
'/learn/private-repositories',
'/learn/create-agent-skills',
'/learn/author-project-skills',
diff --git a/layers/marketing/content/pages/privacy.md b/layers/marketing/content/pages/privacy.md
index 14e04fb4..5d1f1f0c 100644
--- a/layers/marketing/content/pages/privacy.md
+++ b/layers/marketing/content/pages/privacy.md
@@ -1,8 +1,8 @@
---
title: Privacy
description: What skilld.dev stores, why it needs the data, how long it keeps it, and how you delete your account.
-label: Last updated 6 October 2026
-updatedAt: 2026-10-06
+label: Last updated 7 October 2026
+updatedAt: 2026-10-07
---
## In short
@@ -12,6 +12,7 @@ updatedAt: 2026-10-06
- An account is optional. You sign in with [GitHub](https://github.com) to like Skills, watch Repositories, get email, or connect the skilld CLI.
- You can delete your account from your dashboard at any time.
- The skilld CLI sends no telemetry.
+- The MCP server for [ChatGPT](https://chatgpt.com), Claude, and other MCP apps needs no account. It receives only what each tool needs, such as your search text.
## When you visit
@@ -125,6 +126,21 @@ When you sign in with the CLI, skilld stores a record for that token:
If the client sends your operating system and processor type, such as `darwin-arm64`, device sign-in records it. The sign-in page shows it, so you can check which machine asks for access. You can revoke a device in your [dashboard](/me/devices).
+## The MCP server
+
+ChatGPT, Claude, and other MCP apps can search skilld.dev through its MCP server at `https://skilld.dev/api/mcp`. The server needs no account and sets no cookie. Every tool is read only.
+
+When your chat app calls a tool, skilld receives what that tool needs:
+
+- `search_skills` receives your search text and a result count.
+- `get_skill` receives the owner, Repository, and name of one Skill.
+- `get_collection` receives a curator login and a collection slug.
+- `install_command` receives one Skill, Repository, or collection reference.
+
+skilld receives no chat history, no files from the chat, and no account details from the chat app. ChatGPT and Claude call the server from OpenAI or Anthropic servers, so skilld sees their IP address, not yours. Apps on your computer, such as Claude Code or Cursor, call it from your network. Rate limits count requests per IP address, and skilld stores no IP address.
+
+Search text goes to Workers AI, as [Search](#when-you-visit-search) describes. Request logs follow the rules under [When you visit](#when-you-visit). OpenAI and Anthropic handle your chat under their own privacy policies.
+
## Error reports
skilld uses [Sentry](https://sentry.io) to find and fix errors.
@@ -171,4 +187,4 @@ Skills in your public Repositories stay in the registry, because GitHub is their
## Contact
-Email [harlan@harlanzw.com](mailto:harlan@harlanzw.com) with questions about your data. To report a security problem, use [GitHub private vulnerability reporting](https://github.com/skilld-dev/skilld/security/advisories/new).
+Harlan Wilton runs skilld.dev. Email [harlan@harlanzw.com](mailto:harlan@harlanzw.com) with questions about your data. To report a security problem, use [GitHub private vulnerability reporting](https://github.com/skilld-dev/skilld/security/advisories/new).
diff --git a/layers/marketing/content/pages/terms.md b/layers/marketing/content/pages/terms.md
new file mode 100644
index 00000000..b9908467
--- /dev/null
+++ b/layers/marketing/content/pages/terms.md
@@ -0,0 +1,68 @@
+---
+title: Terms
+description: The terms for skilld.dev, the skilld MCP server, the skilld API, and skilld email.
+label: Last updated 7 October 2026
+updatedAt: 2026-10-07
+---
+
+## In short
+
+- skilld.dev is free. It has no paid plan, no ads, and no paid placement.
+- Each Skill belongs to its author. skilld.dev links every Skill to its source on [GitHub](https://github.com).
+- Read a Skill before you run it. skilld does not check that a Skill is safe.
+- skilld.dev comes with no warranty.
+
+## Who runs skilld
+
+Harlan Wilton runs skilld.dev. These terms cover skilld.dev, the skilld MCP server at `https://skilld.dev/api/mcp`, the skilld API, and the email that skilld sends. If you use any of them, you accept these terms.
+
+The skilld CLI is open source under the [MIT License](https://github.com/skilld-dev/skilld/blob/main/LICENSE). That license covers the CLI code.
+
+## Skills and their authors
+
+- Each Skill stays in its author's GitHub Repository. The author owns it, and the license in that Repository applies to it.
+- skilld.dev shows the text of a Skill as its author wrote it, with a link to the source file.
+- Search summaries and tags on skilld.dev are machine-generated. skilld labels them as generated.
+- If you own a Repository and want its Skills removed from skilld.dev, [open an issue](https://github.com/skilld-dev/skilld.dev/issues) or email the address below.
+
+## Running a Skill
+
+A Skill gives instructions to your Agent. Your Agent can then run commands, change files, or fetch content.
+
+- Read a Skill before you run or install it. Every Skill page links the exact `SKILL.md`.
+- Trust labels on skilld.dev say where a Skill comes from. They do not say that a Skill is safe.
+- You are responsible for what your Agent does when it follows a Skill.
+
+## The MCP server and the API
+
+The MCP server lets [ChatGPT](https://chatgpt.com), Claude, and other MCP apps search the registry. It returns search results and commands as text. It runs nothing and installs nothing.
+
+The skilld API serves the same registry to scripts and to the skilld CLI. Account operations need a skilld token.
+
+skilld can change, limit, or stop the MCP server or the API. Both have rate limits. If a client sends too many requests, it gets an error until the limit resets.
+
+## Your account
+
+- You sign in with GitHub, so GitHub's terms also apply to your account.
+- Collections are public. You are responsible for the names and descriptions you give them.
+- You can delete your account at any time from your [dashboard](/me). The [privacy page](/privacy) lists what that deletes.
+- skilld can suspend an account that breaks these terms.
+
+## What you must not do
+
+- Do not send requests at a rate that harms the service, or try to get around a rate limit.
+- Do not try to get access to another person's account or data.
+- Do not use skilld to spread malware, spam, or content that is illegal where you live.
+- Do not use skilld to make a person or an organization look like the author of a Skill they did not write.
+
+## No warranty
+
+skilld provides skilld.dev, the MCP server, and the API "as is", with no warranty of any kind. To the extent the law allows, Harlan Wilton is not liable for any loss that comes from your use of skilld, of a Skill, or of a command that skilld prints. If the law where you live does not allow these limits, they apply as far as that law allows.
+
+## Changes to these terms
+
+skilld can change these terms. The date at the top shows the last change. If you keep using skilld after a change, you accept the new terms.
+
+## Contact
+
+Email [harlan@harlanzw.com](mailto:harlan@harlanzw.com) with questions about these terms. For help with skilld.dev or the MCP server, [open an issue](https://github.com/skilld-dev/skilld.dev/issues).
diff --git a/layers/marketing/nuxt.config.ts b/layers/marketing/nuxt.config.ts
index f5c07df2..c9c2ba0f 100644
--- a/layers/marketing/nuxt.config.ts
+++ b/layers/marketing/nuxt.config.ts
@@ -1,6 +1,6 @@
// Marketing layer: SEO content. Owns /skills (marketing index), /skills/guide,
// /skills/trending, /skills/official, /skills/stats, /frameworks/*,
-// /accessibility, /privacy, /compare/*.
+// /accessibility, /privacy, /terms, /compare/*.
// See docs/adr/0001-url-pillars-and-layers.md
export default defineNuxtConfig({
modules: ['@harlan-zw/comark-content'],
diff --git a/layers/mcp/server/mcp/index.ts b/layers/mcp/server/mcp/index.ts
index 2f87c52b..1b711813 100644
--- a/layers/mcp/server/mcp/index.ts
+++ b/layers/mcp/server/mcp/index.ts
@@ -7,7 +7,7 @@ const MAX_MCP_BODY_BYTES = 64 * 1024
export default defineMcpHandler({
description: 'Search skilld.dev for agent skills and curated collections.',
- instructions: 'Start with search_skills, inspect provenance with get_skill, then return an install command for the user to approve and run.',
+ instructions: 'Search first, inspect provenance before recommending a skill, then return the run command for the user to approve and run. Offer the install command only when the user wants the skill in every session. This server never runs or installs anything.',
tools: (event) => {
const deps: McpToolDeps = {
fetchApi: (input: string, options: RequestInit) => {
@@ -18,6 +18,7 @@ export default defineMcpHandler({
}
return mcpTools.map(tool => defineMcpTool({
name: tool.name,
+ title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema,
annotations: tool.annotations,
diff --git a/layers/mcp/server/routes/.well-known/openai-apps-challenge.get.ts b/layers/mcp/server/routes/.well-known/openai-apps-challenge.get.ts
new file mode 100644
index 00000000..9c29cdad
--- /dev/null
+++ b/layers/mcp/server/routes/.well-known/openai-apps-challenge.get.ts
@@ -0,0 +1,10 @@
+// The OpenAI plugin portal proves that we own skilld.dev before it connects
+// the MCP server. It reads this URL and expects the exact token as plain text.
+export default defineEventHandler((event) => {
+ const token = useRuntimeConfig(event).openaiAppsChallenge
+ if (!token)
+ throw createError({ statusCode: 404, statusMessage: 'No OpenAI domain challenge is set' })
+ setHeader(event, 'content-type', 'text/plain; charset=utf-8')
+ setHeader(event, 'cache-control', 'no-store')
+ return token
+})
diff --git a/layers/mcp/shared/mcp-tools.ts b/layers/mcp/shared/mcp-tools.ts
index c7d95b8f..2f525c2e 100644
--- a/layers/mcp/shared/mcp-tools.ts
+++ b/layers/mcp/shared/mcp-tools.ts
@@ -1,4 +1,5 @@
-import type { FetchImplementation, Result, SkilldFailure } from 'skilld-sdk'
+import type { FetchImplementation, OperationOutput, Result, SkilldFailure } from 'skilld-sdk'
+import type { skillsV1 } from 'skilld-sdk/contract'
import { createSkilldClient } from 'skilld-sdk'
import { z } from 'zod'
import {
@@ -8,6 +9,8 @@ import {
} from './mcp-install-command'
const MAX_RESULT_CHARS = 48_000
+/** Skill pages live on the public site, whatever API origin the tools call. */
+const SITE_ORIGIN = 'https://skilld.dev'
/**
* One spelling for every place an agent reads the accepted refs: the tool
* description, the input schema, and the unrecognized-ref failure.
@@ -50,6 +53,8 @@ export interface McpToolAnnotations {
export interface McpTool {
name: string
+ /** Human-readable name. Claude and ChatGPT show it in the tool list and the approval prompt. */
+ title: string
description: string
inputSchema: Record
annotations: McpToolAnnotations
@@ -101,6 +106,25 @@ function presentResult(
return fail(`${operation} failed. Try again later.`)
}
+/**
+ * The search answer names each Skill by its source only. Every result also
+ * links its Skill page, so provenance is one click away (VISION principle 1).
+ * The long path answers 301 to the canonical page of a one-Skill repository.
+ */
+function withSkillLinks(answer: OperationOutput): Record {
+ return {
+ ...answer,
+ items: answer.items.map((item) => {
+ const { owner, repository, selector } = item.source
+ return {
+ ...item,
+ pageUrl: `${SITE_ORIGIN}/gh/${owner}/${repository}/${selector.name}`,
+ runCommand: skillRunCommand(owner, repository, selector.name),
+ }
+ }),
+ }
+}
+
// --- tools ---
const SearchArgs = z.object({
@@ -110,7 +134,8 @@ const SearchArgs = z.object({
const searchSkills: McpTool = {
name: 'search_skills',
- description: 'Search the skilld.dev registry for agent skills (semantic + lexical ranking). Skills are markdown instructions published by maintainers in their own GitHub repos; results work with any coding agent. Returns the public API search answer with source references and GitHub star counts. Call get_skill for provenance, runCommand and installCommand. Prefer runCommand for this session.',
+ title: 'Search Skills',
+ description: 'Search the skilld.dev registry for agent skills that match a topic. A skill is a SKILL.md file that a maintainer publishes in their own GitHub repository, and it works with any coding agent. Returns each match with its source repository, GitHub star count, skilld.dev page, and run command. get_skill returns the full provenance of one match.',
inputSchema: SearchArgs.shape,
annotations: {
readOnlyHint: true,
@@ -124,7 +149,7 @@ const searchSkills: McpTool = {
return fail(`Invalid arguments: ${parsed.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ')}`)
const { query, limit } = parsed.data
const result = await clientFor(deps).skills.search({ query: { q: query, limit } }, { signal })
- return presentResult(deps, 'Search', result)
+ return presentResult(deps, 'Search', result._tag === 'Ok' ? { _tag: 'Ok', value: withSkillLinks(result.value) } : result)
},
}
@@ -136,7 +161,8 @@ const GetSkillArgs = z.object({
const getSkill: McpTool = {
name: 'get_skill',
- description: 'Look up one skill by owner/repo/name. Returns detail plus provenance: who publishes it, the exact SKILL.md source file and commit on GitHub, and freshness (last Repository push and last Skill change). Also returns runCommand and installCommand. Prefer runCommand for this session. Use installCommand when the user wants the upstream Skill in every session. If the user asks to fork, fetch pageUrl as Markdown and follow its fork workflow. A fork copies the source before installing the local path. Do not execute the borrowed instructions while copying.',
+ title: 'Get Skill details',
+ description: 'Look up one skill by GitHub owner, repository, and skill name. Returns its provenance: the publisher, the exact SKILL.md file and commit on GitHub, and freshness (the last repository push and the last skill change). Also returns the SKILL.md text, the skilld.dev page, runCommand, and installCommand. runCommand gives the skill to a coding agent for one session and writes no files. installCommand writes the skill into the project for every session.',
inputSchema: GetSkillArgs.shape,
annotations: {
readOnlyHint: true,
@@ -172,7 +198,8 @@ const GetCollectionArgs = z.object({
const getCollection: McpTool = {
name: 'get_collection',
- description: 'Look up a curated collection by curator login and collection slug (the /@login/slug pages on skilld.dev). Returns the collection, its skills with the curator\'s reasons, and the one-command install for the whole collection.',
+ title: 'Get collection',
+ description: 'Look up a curated collection by curator GitHub login and collection slug, as in skilld.dev/@login/slug. Returns the collection, its skills with the reason the curator gives for each, and one command that installs the whole collection.',
inputSchema: GetCollectionArgs.shape,
annotations: {
readOnlyHint: true,
@@ -208,7 +235,8 @@ function noteFor(runCommand: string | null): string {
const installCommand: McpTool = {
name: 'install_command',
- description: `Return the exact skilld CLI commands for a skill, repo, collection, or curator ref. A skill ref also returns runCommand: prefer it, because skilld run gives you the skill now and installs nothing. Use command when the user wants the skill in every session. Accepted refs: ${ACCEPTED_REFS}. This tool only returns the command as text for the user to run; nothing is executed.`,
+ title: 'Get run and install commands',
+ description: `Return the exact skilld CLI commands for a skill, repository, collection, or curator ref. For a single skill, runCommand gives the skill to a coding agent for one session and writes no files. command installs the skills into the project. Accepted refs: ${ACCEPTED_REFS}. This tool returns text only. It runs and installs nothing.`,
inputSchema: InstallCommandArgs.shape,
annotations: {
readOnlyHint: true,
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 6e1d5d49..a22db4f4 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -366,6 +366,11 @@ export default defineNuxtConfig({
instructions: 'Search first, inspect provenance before recommending a skill, then return the run command for the user to approve and run. Offer the install command only when the user wants the skill in every session. This server never runs or installs anything.',
sessions: false,
browserRedirect: '/',
+ // The toolkit rejects a request whose Origin header is another site. Server-side
+ // clients send none. The Claude and ChatGPT web apps may, so they are listed.
+ security: {
+ allowedOrigins: ['https://skilld.dev', 'https://claude.ai', 'https://claude.com', 'https://chatgpt.com', 'https://chat.openai.com'],
+ },
},
app: {
@@ -419,6 +424,10 @@ export default defineNuxtConfig({
// Shared with the skill-harness Worker as SKILLGEN_SITE_TOKEN. It reads Skillgen opt-ins.
skillgenToken: '',
publicSiteUrl: 'https://skilld.dev',
+ // The OpenAI plugin portal's domain-verification token. Set the Worker
+ // secret NUXT_OPENAI_APPS_CHALLENGE. /.well-known/openai-apps-challenge
+ // serves it, and answers 404 while it is empty.
+ openaiAppsChallenge: '',
// Task search, the search box's opt-in model answer. Set the Worker
// variable NUXT_TASK_SEARCH_ENABLED=false to switch it off without a
// deploy. The budget is micro-dollars per UTC day across every visitor:
diff --git a/test/unit/developer-setup.test.ts b/test/unit/developer-setup.test.ts
index 8efae524..2181f27c 100644
--- a/test/unit/developer-setup.test.ts
+++ b/test/unit/developer-setup.test.ts
@@ -3,9 +3,17 @@ import type { ApiSampleCall } from '../../layers/marketing/app/utils/developer-s
import { createSkilldClient } from 'skilld-sdk'
import { skilldV1Protocol } from 'skilld-sdk/contract'
import { describe, expect, it } from 'vitest'
-import { apiSampleCalls, curlCall, cursorInstallUrl, vscodeInstallUrl } from '../../layers/marketing/app/utils/developer-setup'
+import { apiSampleCalls, claudeConnectorUrl, curlCall, cursorInstallUrl, vscodeInstallUrl } from '../../layers/marketing/app/utils/developer-setup'
describe('mCP install links', () => {
+ it('opens Claude\'s custom connector dialog with the skilld name and the server URL', () => {
+ const link = new URL(claudeConnectorUrl('https://example.test/api/mcp?x=1'))
+ expect(`${link.origin}${link.pathname}`).toBe('https://claude.ai/customize/connectors')
+ expect(link.searchParams.get('modal')).toBe('add-custom-connector')
+ expect(link.searchParams.get('connectorName')).toBe('skilld')
+ expect(link.searchParams.get('connectorUrl')).toBe('https://example.test/api/mcp?x=1')
+ })
+
it('gives Cursor the server URL as base64 JSON under the skilld name', () => {
const link = new URL(cursorInstallUrl('https://example.test/api/mcp?x=1'))
expect(`${link.protocol}//${link.host}${link.pathname}`).toBe('cursor://anysphere.cursor-deeplink/mcp/install')
diff --git a/test/unit/mcp-discovery.test.ts b/test/unit/mcp-discovery.test.ts
index 8e82c414..3f294bc3 100644
--- a/test/unit/mcp-discovery.test.ts
+++ b/test/unit/mcp-discovery.test.ts
@@ -34,8 +34,16 @@ describe('mCP public SDK discovery', () => {
const result = await runTool('search_skills', { query: 'tailwind', limit: 1 }, deps(fetchApi), signal)
- expect(result.structuredContent).toEqual(response)
- expect(JSON.parse(result.content[0]!.text)).toEqual(response)
+ const linked = {
+ ...response,
+ items: response.items.map(item => ({
+ ...item,
+ pageUrl: `https://skilld.dev/gh/${item.source.owner}/${item.source.repository}/${item.source.selector.name}`,
+ runCommand: `npx skilld run ${item.source.owner}/${item.source.repository}/${item.source.selector.name}`,
+ })),
+ }
+ expect(result.structuredContent).toEqual(linked)
+ expect(JSON.parse(result.content[0]!.text)).toEqual(linked)
const [input, options] = fetchApi.mock.calls[0]!
const url = new URL(input)
expect(url.pathname).toBe('/api/v1/skills')