{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mcp-agent",
  "title": "MCP Agent",
  "description": "An MCP-backed workspace that inspects installed project docs and optional local Next.js runtime state through AI SDK MCP clients.",
  "dependencies": [
    "@ai-sdk/mcp",
    "@ai-sdk/react",
    "@base-ui/react",
    "@modelcontextprotocol/sdk",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "lucide-react",
    "next-devtools-mcp",
    "zod"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json",
    "https://elements.ai-sdk.dev/api/registry/reasoning.json",
    "https://elements.ai-sdk.dev/api/registry/shimmer.json",
    "https://elements.ai-sdk.dev/api/registry/tool.json"
  ],
  "files": [
    {
      "path": "registry/mcp-agent/app/demos/mcp-agent/page.tsx",
      "content": "import { McpAgentScreen } from \"@/components/mcp-agent/mcp-agent-screen\";\n\nexport default function McpAgentPage() {\n  return <McpAgentScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/mcp-agent/page.tsx"
    },
    {
      "path": "registry/mcp-agent/app/api/demos/mcp-agent/route.ts",
      "content": "import { handleMcpAgentRequest } from \"@/lib/mcp-agent/server/runtime\";\n\nexport const runtime = \"nodejs\";\n\nexport function POST(request: Request) {\n  return handleMcpAgentRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/mcp-agent/route.ts"
    },
    {
      "path": "registry/mcp-agent/app/api/demos/mcp-agent/mcp/route.ts",
      "content": "import { handleProjectDocsMcpRequest } from \"@/lib/mcp-agent/server/project-mcp-server\";\n\nexport const runtime = \"nodejs\";\n\nexport function DELETE(request: Request) {\n  return handleProjectDocsMcpRequest(request);\n}\n\nexport function GET(request: Request) {\n  return handleProjectDocsMcpRequest(request);\n}\n\nexport function POST(request: Request) {\n  return handleProjectDocsMcpRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/mcp-agent/mcp/route.ts"
    },
    {
      "path": "registry/mcp-agent/components/mcp-agent/mcp-agent-screen.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { ArrowLeft } from \"lucide-react\";\n\nimport { getMcpAgentRuntimeState } from \"@/lib/mcp-agent/server/runtime\";\nimport { McpAgentWorkspace } from \"./mcp-agent-workspace\";\n\nexport function McpAgentScreen() {\n  const runtimeState = getMcpAgentRuntimeState();\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6\">\n        <header className=\"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\">\n          <div className=\"space-y-2\">\n            <Breadcrumb>\n              <BreadcrumbList className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                <BreadcrumbItem>\n                  <BreadcrumbLink\n                    aria-label=\"Back to demos\"\n                    className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n                    href=\"/\"\n                  >\n                    <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                    <span>Demo</span>\n                  </BreadcrumbLink>\n                </BreadcrumbItem>\n                <BreadcrumbSeparator className=\"text-muted-foreground\">\n                  /\n                </BreadcrumbSeparator>\n                <BreadcrumbItem>\n                  <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n                    MCP Runtime Doctor Agent\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              Ask one agent to inspect project docs and local Next.js runtime\n              state through MCP\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This workspace connects AI SDK MCP clients to a built-in project\n              docs server and an optional Next.js dev MCP server, then exposes\n              the discovered tools to a ToolLoopAgent.\n            </p>\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <Badge variant=\"outline\">{runtimeState.statusLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.chatModel}</Badge>\n          </div>\n        </header>\n\n        <div className=\"lg:h-svh\">\n          <McpAgentWorkspace\n            chatModel={runtimeState.chatModel}\n            configuredServers={runtimeState.configuredServers}\n            configuredTools={runtimeState.configuredTools}\n            isChatAvailable={runtimeState.isChatAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            setupMessage={runtimeState.setupMessage}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/mcp-agent/mcp-agent-screen.tsx"
    },
    {
      "path": "registry/mcp-agent/components/mcp-agent/mcp-agent-workspace.tsx",
      "content": "\"use client\";\n\nimport { StopIcon, WrenchIcon } from \"@phosphor-icons/react\";\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport type { McpAgentRuntimeState } from \"@/lib/mcp-agent/server/runtime\";\nimport { McpAgentAssistantTrace } from \"./mcp-agent-assistant-trace\";\nimport { getTextContent, mcpAgentSamplePrompts } from \"./mcp-agent-model\";\nimport { McpRuntimeSidebar } from \"./mcp-runtime-sidebar\";\nimport { useMcpAgentChat } from \"./use-mcp-agent-chat\";\n\ninterface McpAgentWorkspaceProps {\n  chatModel: string;\n  configuredServers: McpAgentRuntimeState[\"configuredServers\"];\n  configuredTools: McpAgentRuntimeState[\"configuredTools\"];\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n}\n\nexport function McpAgentWorkspace({\n  chatModel,\n  configuredServers,\n  configuredTools,\n  isChatAvailable,\n  nodeVersion,\n  setupMessage,\n}: McpAgentWorkspaceProps) {\n  const {\n    error,\n    hasMessages,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    status,\n    stop,\n  } = useMcpAgentChat();\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_22rem] lg:grid-rows-[minmax(0,1fr)]\">\n      <section className=\"flex min-h-[70svh] flex-col border border-foreground/10 bg-background lg:h-full lg:min-h-0\">\n        {isChatAvailable ? null : (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-muted-foreground text-xs/relaxed\">\n            {setupMessage}\n          </div>\n        )}\n\n        {error ? (\n          <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n            {error.message}\n          </div>\n        ) : null}\n\n        <Conversation className=\"min-h-0\">\n          <ConversationContent className=\"mx-auto flex w-full max-w-4xl flex-1 gap-6 px-4 pt-6 pb-[3lh]\">\n            {hasMessages ? (\n              messages.map((message, index) => (\n                <Message from={message.role} key={message.id}>\n                  <MessageContent\n                    className={cn(\n                      \"space-y-4\",\n                      message.role === \"assistant\" ? \"max-w-4xl\" : \"max-w-2xl\"\n                    )}\n                  >\n                    {message.role === \"assistant\" ? (\n                      <McpAgentAssistantTrace\n                        isLastMessage={index === messages.length - 1}\n                        isStreaming={isBusy}\n                        message={message}\n                      />\n                    ) : (\n                      <MessageResponse>\n                        {getTextContent(message)}\n                      </MessageResponse>\n                    )}\n                  </MessageContent>\n                </Message>\n              ))\n            ) : (\n              <ConversationEmptyState\n                description=\"Ask about project docs, demo planning, or the local Next.js runtime. The agent will choose the relevant MCP tools without a mode switch.\"\n                icon={<WrenchIcon className=\"size-5\" />}\n                title=\"MCP tool workspace is ready\"\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <div className=\"border-foreground/10 border-t px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-4xl\">\n            <PromptInput onSubmit={({ text }) => sendMessage({ text })}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!isChatAvailable || isBusy}\n                  placeholder=\"Ask about project docs, demos, or local Next.js runtime state.\"\n                />\n              </PromptInputBody>\n              <PromptInputFooter className=\"flex items-center justify-between gap-3 border-foreground/10 border-t px-3 py-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">ToolLoopAgent</Badge>\n                  <Badge variant=\"outline\">MCP</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  {isBusy ? (\n                    <Button\n                      onClick={stop}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <StopIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  {hasMessages ? (\n                    <Button\n                      onClick={() => regenerate()}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      Retry\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit\n                    disabled={!isChatAvailable}\n                    status={status}\n                  />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {mcpAgentSamplePrompts.map((prompt) => (\n                  <Button\n                    key={prompt}\n                    onClick={() => sendMessage({ text: prompt })}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <WrenchIcon className=\"size-3.5\" />\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </section>\n\n      <McpRuntimeSidebar\n        configuredServers={configuredServers}\n        configuredTools={configuredTools}\n        nodeVersion={nodeVersion}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/mcp-agent/mcp-agent-workspace.tsx"
    },
    {
      "path": "registry/mcp-agent/components/mcp-agent/mcp-agent-assistant-trace.tsx",
      "content": "\"use client\";\n\nimport { MessageResponse } from \"@/components/ai-elements/message\";\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/components/ai-elements/reasoning\";\nimport { Shimmer } from \"@/components/ai-elements/shimmer\";\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n} from \"@/components/ai-elements/tool\";\nimport type { UIMessage } from \"ai\";\n\nimport {\n  getReasoningText,\n  getTextContent,\n  getToolParts,\n} from \"./mcp-agent-model\";\n\ninterface McpAgentAssistantTraceProps {\n  isLastMessage: boolean;\n  isStreaming: boolean;\n  message: UIMessage;\n}\n\nexport function McpAgentAssistantTrace({\n  isLastMessage,\n  isStreaming,\n  message,\n}: McpAgentAssistantTraceProps) {\n  const text = getTextContent(message);\n  const reasoningText = getReasoningText(message);\n  const toolParts = getToolParts(message);\n  const hasReasoning = reasoningText.length > 0;\n  const hasText = text.length > 0;\n  const lastPart = message.parts.at(-1);\n  const isReasoningStreaming =\n    isLastMessage && isStreaming && lastPart?.type === \"reasoning\";\n  const showThinking =\n    isLastMessage && isStreaming && !hasReasoning && !hasText;\n\n  return (\n    <>\n      {hasReasoning ? (\n        <Reasoning className=\"w-full\" isStreaming={isReasoningStreaming}>\n          <ReasoningTrigger />\n          <ReasoningContent>{reasoningText}</ReasoningContent>\n        </Reasoning>\n      ) : null}\n\n      {toolParts.map((part) => (\n        <Tool key={part.toolCallId}>\n          {part.type === \"dynamic-tool\" ? (\n            <ToolHeader\n              state={part.state}\n              toolName={part.toolName}\n              type={part.type}\n            />\n          ) : (\n            <ToolHeader state={part.state} type={part.type} />\n          )}\n          <ToolContent>\n            {part.input ? <ToolInput input={part.input} /> : null}\n            <ToolOutput errorText={part.errorText} output={part.output} />\n          </ToolContent>\n        </Tool>\n      ))}\n\n      {hasText ? <MessageResponse>{text}</MessageResponse> : null}\n      {showThinking ? <Shimmer className=\"text-sm\">Thinking...</Shimmer> : null}\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/mcp-agent/mcp-agent-assistant-trace.tsx"
    },
    {
      "path": "registry/mcp-agent/components/mcp-agent/mcp-agent-model.ts",
      "content": "import { isReasoningUIPart, isToolUIPart, type UIMessage } from \"ai\";\n\nexport const mcpAgentSamplePrompts = [\n  \"Review the project docs and tell me what MCP agent demo should cover.\",\n  \"Check the local Next.js runtime for current errors, then explain what you found.\",\n  \"List the ready demos and recommend the next checklist item.\",\n] as const;\n\nexport function getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nexport function getReasoningText(message: UIMessage) {\n  return message.parts\n    .filter(isReasoningUIPart)\n    .map((part) => part.text)\n    .join(\"\\n\\n\");\n}\n\nexport function getToolParts(message: UIMessage) {\n  return message.parts.filter(isToolUIPart);\n}\n",
      "type": "registry:component",
      "target": "@components/mcp-agent/mcp-agent-model.ts"
    },
    {
      "path": "registry/mcp-agent/components/mcp-agent/mcp-runtime-sidebar.tsx",
      "content": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\n\nimport type {\n  ConfiguredMcpServer,\n  McpAgentRuntimeState,\n} from \"@/lib/mcp-agent/server/runtime\";\n\ninterface McpRuntimeSidebarProps {\n  configuredServers: ConfiguredMcpServer[];\n  configuredTools: McpAgentRuntimeState[\"configuredTools\"];\n  nodeVersion: string;\n}\n\nexport function McpRuntimeSidebar({\n  configuredServers,\n  configuredTools,\n  nodeVersion,\n}: McpRuntimeSidebarProps) {\n  return (\n    <aside className=\"border border-foreground/10 bg-background p-4 lg:h-full lg:min-h-0 lg:overflow-y-auto\">\n      <div className=\"space-y-5\">\n        <div>\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n            Runtime\n          </p>\n          <p className=\"mt-1 font-medium text-sm\">{nodeVersion}</p>\n          <p className=\"mt-1 text-muted-foreground text-sm\">\n            AI SDK MCP client\n          </p>\n        </div>\n\n        <div className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n            Connected MCP servers\n          </p>\n          {configuredServers.map((server) => (\n            <div\n              className=\"space-y-2 border border-foreground/10 p-3\"\n              key={server.name}\n            >\n              <div className=\"flex flex-wrap items-center gap-2\">\n                <p className=\"font-medium text-sm\">{server.name}</p>\n                <Badge variant=\"secondary\">{server.transport}</Badge>\n              </div>\n              <p className=\"text-muted-foreground text-sm/relaxed\">\n                {server.description}\n              </p>\n              <p className=\"text-muted-foreground text-xs\">\n                {server.requiredSetup}\n              </p>\n            </div>\n          ))}\n        </div>\n\n        <div className=\"space-y-2\">\n          <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n            Available tools\n          </p>\n          {configuredTools.map((tool) => (\n            <div\n              className=\"space-y-1 border border-foreground/10 p-3\"\n              key={tool.name}\n            >\n              <div className=\"flex flex-wrap items-center gap-2\">\n                <p className=\"font-medium text-sm\">{tool.name}</p>\n                <Badge variant=\"outline\">{tool.server}</Badge>\n              </div>\n              <p className=\"text-muted-foreground text-sm/relaxed\">\n                {tool.description}\n              </p>\n            </div>\n          ))}\n        </div>\n      </div>\n    </aside>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/mcp-agent/mcp-runtime-sidebar.tsx"
    },
    {
      "path": "registry/mcp-agent/components/mcp-agent/use-mcp-agent-chat.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport { DefaultChatTransport } from \"ai\";\nimport { useState } from \"react\";\n\nexport function useMcpAgentChat() {\n  const [chat] = useState(\n    () =>\n      new Chat({\n        transport: new DefaultChatTransport({\n          api: \"/api/demos/mcp-agent\",\n        }),\n      })\n  );\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy =\n    controller.status === \"submitted\" || controller.status === \"streaming\";\n\n  return {\n    ...controller,\n    chat,\n    hasMessages,\n    isBusy,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/mcp-agent/use-mcp-agent-chat.ts"
    },
    {
      "path": "registry/mcp-agent/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/chat.ts",
      "content": "import {\n  createAgentUIStreamResponse,\n  stepCountIs,\n  ToolLoopAgent,\n  type UIMessage,\n} from \"ai\";\nimport { createMcpAgentGateway, getMcpAgentEnv } from \"./env\";\nimport { createMcpAgentToolbox } from \"./mcp-clients\";\nimport { formatMcpRuntimeSummary } from \"./mcp-toolbox\";\nimport { MCP_AGENT_PROVIDER_OPTIONS, resolveMcpAgentChatModel } from \"./model\";\n\ntype DemoEnv = Record<string, string | undefined>;\n\nexport const mcpAgentInstructions = [\n  \"You are the MCP Runtime Doctor Agent demo for a product and engineering team.\",\n  \"Use the connected MCP tools naturally when they can ground the answer in project docs, demo metadata, or local Next.js runtime state.\",\n  \"Do not ask the user to choose a mode. Infer whether project docs, runtime diagnostics, or both are relevant from the message.\",\n  \"When a requested MCP server is unavailable, explain the visible status and continue with available MCP tools when useful.\",\n  \"Keep answers concise and cite the MCP tool evidence you used in plain language.\",\n].join(\" \");\n\nexport async function streamMcpAgent(\n  messages: UIMessage[],\n  {\n    env = getMcpAgentEnv(),\n    origin,\n  }: {\n    env?: DemoEnv;\n    origin: string;\n  }\n) {\n  const gateway = createMcpAgentGateway(env);\n  const chatModel = resolveMcpAgentChatModel(env);\n  const toolbox = await createMcpAgentToolbox({ origin });\n  const agent = new ToolLoopAgent({\n    instructions: mcpAgentInstructions,\n    model: gateway(chatModel),\n    prepareCall: ({ ...call }) => ({\n      ...call,\n      instructions: [\n        mcpAgentInstructions,\n        `Connected MCP servers and tools:\\n${formatMcpRuntimeSummary(\n          toolbox.summary\n        )}`,\n      ].join(\"\\n\\n\"),\n    }),\n    providerOptions: MCP_AGENT_PROVIDER_OPTIONS,\n    stopWhen: stepCountIs(20),\n    tools: toolbox.tools,\n  });\n\n  return await createAgentUIStreamResponse({\n    agent,\n    onFinish: async () => {\n      await toolbox.close();\n    },\n    sendReasoning: true,\n    uiMessages: messages,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/chat.ts"
    },
    {
      "path": "registry/mcp-agent/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/env-source.ts",
      "content": "export function getMcpAgentAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/env-source.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/env.ts",
      "content": "import { getMcpAgentAppEnv } from \"./env-source\";\nimport {\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n} from \"@/lib/ai-gateway/contract\";\n\nexport const DEFAULT_MCP_AGENT_CHAT_MODEL = \"openai/gpt-5-mini\";\n\nexport type McpAgentEnv = AiGatewayEnvRecord;\n\nexport type McpAgentConfig = AiGatewayContractConfig;\n\nexport type McpAgentSetupState = AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type McpAgentGateway = ReturnType<typeof createAiGatewayFromContract>;\n\nconst mcpAgentContract = {\n  defaultChatModel: DEFAULT_MCP_AGENT_CHAT_MODEL,\n  missingApiKeyError: \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the MCP agent.\",\n  missingApiKeyIssue: \"AI_GATEWAY_API_KEY is missing. The demo can render, but MCP agent chat requests will fail until it is configured.\",\n} as const;\n\nexport function getMcpAgentEnv(): McpAgentEnv {\n  return getMcpAgentAppEnv();\n}\n\nexport function getMcpAgentConfig(\n  env: McpAgentEnv = getMcpAgentEnv()\n): McpAgentConfig {\n  return readAiGatewayContractConfig(env, mcpAgentContract);\n}\n\nexport function getMcpAgentSetupState(\n  env: McpAgentEnv = getMcpAgentEnv()\n): McpAgentSetupState {\n  return buildAiGatewayContractSetupState(env, mcpAgentContract);\n}\n\nexport function createMcpAgentGateway(\n  env: McpAgentEnv = getMcpAgentEnv()\n): McpAgentGateway {\n  return createAiGatewayFromContract(env, mcpAgentContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/env.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/mcp-clients.ts",
      "content": "import { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nimport { createMCPClient, type MCPClient } from \"@ai-sdk/mcp\";\nimport { Experimental_StdioMCPTransport } from \"@ai-sdk/mcp/mcp-stdio\";\nimport type { ToolSet } from \"ai\";\n\nimport {\n  closeMcpClients,\n  createMcpRuntimeSummary,\n  type McpServerSummary,\n  type McpToolbox,\n  namespaceMcpTools,\n} from \"./mcp-toolbox\";\n\nconst requireFromHere = createRequire(import.meta.url);\n\nfunction resolveNextDevtoolsMcpBin() {\n  const packageJsonPath = requireFromHere.resolve(\n    \"next-devtools-mcp/package.json\"\n  );\n\n  return path.join(path.dirname(packageJsonPath), \"dist/index.js\");\n}\n\nasync function createProjectDocsClient(origin: string) {\n  return await createMCPClient({\n    clientName: \"mcp-agent-project-docs-client\",\n    transport: {\n      type: \"http\",\n      url: new URL(\"/api/demos/mcp-agent/mcp\", origin).toString(),\n    },\n  });\n}\n\nasync function createNextRuntimeClient() {\n  return await createMCPClient({\n    clientName: \"mcp-agent-nextjs-runtime-client\",\n    transport: new Experimental_StdioMCPTransport({\n      args: [resolveNextDevtoolsMcpBin()],\n      command: process.execPath,\n      stderr: \"pipe\",\n    }),\n  });\n}\n\nasync function appendClientTools({\n  client,\n  clients,\n  prefix,\n  serverName,\n  summaries,\n  tools,\n  transport,\n}: {\n  client: MCPClient;\n  clients: MCPClient[];\n  prefix: string;\n  serverName: string;\n  summaries: McpServerSummary[];\n  tools: ToolSet;\n  transport: \"http\" | \"stdio\";\n}) {\n  clients.push(client);\n  const rawTools = (await client.tools()) as ToolSet;\n  const namespacedTools = namespaceMcpTools(prefix, rawTools);\n  const toolNames = Object.keys(namespacedTools);\n\n  Object.assign(tools, namespacedTools);\n  summaries.push({\n    instructions: client.instructions,\n    name: serverName,\n    status: \"ready\",\n    toolNames,\n    transport,\n  });\n}\n\nfunction errorReason(error: unknown) {\n  return error instanceof Error\n    ? error.message\n    : \"Unknown MCP connection error.\";\n}\n\nexport async function createMcpAgentToolbox({\n  origin,\n}: {\n  origin: string;\n}): Promise<McpToolbox> {\n  const clients: MCPClient[] = [];\n  const summaries: McpServerSummary[] = [];\n  const tools: ToolSet = {};\n\n  await appendClientTools({\n    client: await createProjectDocsClient(origin),\n    clients,\n    prefix: \"project\",\n    serverName: \"project-docs\",\n    summaries,\n    tools,\n    transport: \"http\",\n  });\n\n  try {\n    await appendClientTools({\n      client: await createNextRuntimeClient(),\n      clients,\n      prefix: \"nextjs\",\n      serverName: \"nextjs-runtime\",\n      summaries,\n      tools,\n      transport: \"stdio\",\n    });\n  } catch (error) {\n    summaries.push({\n      name: \"nextjs-runtime\",\n      reason: errorReason(error),\n      status: \"unavailable\",\n      toolNames: [],\n      transport: \"stdio\",\n    });\n  }\n\n  return {\n    close: () => closeMcpClients(clients),\n    summary: createMcpRuntimeSummary(summaries),\n    tools,\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/mcp-clients.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/mcp-toolbox.ts",
      "content": "import type { MCPClient } from \"@ai-sdk/mcp\";\nimport type { ToolSet } from \"ai\";\n\nexport type McpServerStatus = \"ready\" | \"unavailable\";\nexport type McpTransportKind = \"http\" | \"stdio\";\n\nexport interface McpServerSummary {\n  instructions?: string;\n  name: string;\n  reason?: string;\n  status: McpServerStatus;\n  toolNames: string[];\n  transport: McpTransportKind;\n}\n\nexport interface McpRuntimeSummary {\n  availableTools: string[];\n  servers: McpServerSummary[];\n}\n\nexport interface McpToolbox {\n  close: () => Promise<void>;\n  summary: McpRuntimeSummary;\n  tools: ToolSet;\n}\n\nexport function namespaceMcpTools(prefix: string, tools: ToolSet): ToolSet {\n  return Object.fromEntries(\n    Object.entries(tools).map(([name, tool]) => [`${prefix}__${name}`, tool])\n  );\n}\n\nexport function createMcpRuntimeSummary(\n  servers: McpServerSummary[]\n): McpRuntimeSummary {\n  return {\n    availableTools: servers.flatMap((server) => server.toolNames),\n    servers,\n  };\n}\n\nexport function formatMcpRuntimeSummary(summary: McpRuntimeSummary): string {\n  return summary.servers\n    .map((server) => {\n      const header = `${server.name} [${server.status}, ${server.transport}]`;\n      const statusLine =\n        server.status === \"ready\"\n          ? header\n          : `${header}: ${server.reason ?? \"Unavailable.\"}`;\n      const toolLines =\n        server.toolNames.length > 0\n          ? server.toolNames.map((toolName) => `- ${toolName}`).join(\"\\n\")\n          : \"- no tools available\";\n      const instructions = server.instructions\n        ? `\\nInstructions: ${server.instructions}`\n        : \"\";\n\n      return `${statusLine}${instructions}\\nTools:\\n${toolLines}`;\n    })\n    .join(\"\\n\\n\");\n}\n\nexport async function closeMcpClients(clients: MCPClient[]) {\n  await Promise.allSettled(clients.map((client) => client.close()));\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/mcp-toolbox.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/model.ts",
      "content": "import { getMcpAgentEnv } from \"./env\";\n\nconst DEFAULT_MCP_AGENT_CHAT_MODEL = \"openai/gpt-5-mini\";\n\nexport const MCP_AGENT_PROVIDER_OPTIONS = {\n  openai: {\n    reasoningEffort: \"medium\",\n    reasoningSummary: \"auto\",\n  },\n} as const;\n\ntype McpAgentEnv = Record<string, string | undefined>;\n\nexport function resolveMcpAgentChatModel(\n  env: McpAgentEnv = getMcpAgentEnv()\n): string {\n  return env.AI_GATEWAY_CHAT_MODEL || DEFAULT_MCP_AGENT_CHAT_MODEL;\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/model.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/project-docs-catalog.ts",
      "content": "import { existsSync } from \"node:fs\";\nimport { readFile, readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport const projectDemoPatterns = [\n  \"foundation\",\n  \"rag\",\n  \"loop\",\n  \"tools\",\n  \"skills\",\n  \"sandbox\",\n  \"multimodal\",\n  \"structured-output\",\n  \"mcp\",\n  \"generative-ui\",\n] as const;\n\nexport type ProjectDemoPattern = (typeof projectDemoPatterns)[number];\n\nexport const projectDemoStatuses = [\"ready\", \"roadmap\"] as const;\n\nexport type ProjectDemoStatus = (typeof projectDemoStatuses)[number];\n\nexport interface ProjectDocsCatalogEntry {\n  docsPath: string;\n  href: `/demos/${string}`;\n  pattern: ProjectDemoPattern;\n  readmePath?: string;\n  slug: string;\n  source: string;\n  status: ProjectDemoStatus;\n  summary: string;\n  title: string;\n}\n\nconst excludedFrontendDocSlugs = new Set([\n  \"DOCS\",\n  \"index\",\n  \"agent-demo-structure\",\n  \"ai-sdk-recipes-checklist\",\n  \"homepage-gallery\",\n  \"registry-sync\",\n  \"shadcn-registry-distribution\",\n  \"ultra-chatbot-agent-source-checklist\",\n  \"workspace-ui\",\n]);\nconst roadmapSlugs = new Set([\"openai-agents-sdk-demo\", \"ultra-chatbot-agent\"]);\nconst frontmatterPattern = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/;\nconst headingPattern = /^#\\s+(.+)$/m;\nconst frontmatterDescriptionPattern = /^description:\\s*(.+)$/m;\nconst frontmatterTitlePattern = /^title:\\s*(.+)$/m;\n\nfunction toRepoPath(root: string, absolutePath: string) {\n  return path.relative(root, absolutePath);\n}\n\nfunction stripFrontmatter(content: string) {\n  const match = content.match(frontmatterPattern);\n\n  return match ? content.slice(match[0].length).trim() : content.trim();\n}\n\nfunction humanizeSlug(slug: string) {\n  return slug\n    .split(\"-\")\n    .filter(Boolean)\n    .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n    .join(\" \");\n}\n\nfunction inferPattern(slug: string): ProjectDemoPattern {\n  if (slug.includes(\"mcp\")) {\n    return \"mcp\";\n  }\n\n  if (slug.includes(\"skills\")) {\n    return \"skills\";\n  }\n\n  if (slug.includes(\"sandbox\")) {\n    return \"sandbox\";\n  }\n\n  if (slug.includes(\"loop\")) {\n    return \"loop\";\n  }\n\n  if (slug.includes(\"rag\")) {\n    return \"rag\";\n  }\n\n  if (slug.includes(\"multimodal\")) {\n    return \"multimodal\";\n  }\n\n  if (slug.includes(\"generation\")) {\n    return \"structured-output\";\n  }\n\n  return \"tools\";\n}\n\nfunction inferStatus(slug: string): ProjectDemoStatus {\n  return roadmapSlugs.has(slug) ? \"roadmap\" : \"ready\";\n}\n\nfunction firstParagraph(body: string) {\n  return body\n    .split(/\\r?\\n/)\n    .map((line) => line.trim())\n    .filter(Boolean)\n    .find(\n      (line) =>\n        !line.startsWith(\"#\") &&\n        !line.startsWith(\"- \") &&\n        !line.startsWith(\"* \")\n    );\n}\n\nexport function findProjectRoot(start = process.cwd()) {\n  let current = start;\n  let packageRoot: string | null = null;\n\n  while (current !== path.dirname(current)) {\n    if (!packageRoot && existsSync(path.join(current, \"package.json\"))) {\n      packageRoot = current;\n    }\n\n    if (\n      existsSync(path.join(current, \"AGENTS.md\")) ||\n      existsSync(path.join(current, \"pnpm-workspace.yaml\")) ||\n      existsSync(path.join(current, \"docs\"))\n    ) {\n      return current;\n    }\n\n    current = path.dirname(current);\n  }\n\n  return packageRoot ?? start;\n}\n\nasync function readDocsMetadata(absolutePath: string, slug: string) {\n  const content = await readFile(absolutePath, \"utf8\");\n  const frontmatter = content.match(frontmatterPattern)?.[1] ?? \"\";\n  const body = stripFrontmatter(content);\n\n  return {\n    summary:\n      frontmatter.match(frontmatterDescriptionPattern)?.[1]?.trim() ??\n      firstParagraph(body) ??\n      `${humanizeSlug(slug)} project docs.`,\n    title:\n      frontmatter.match(frontmatterTitlePattern)?.[1]?.trim() ??\n      body.match(headingPattern)?.[1]?.trim() ??\n      humanizeSlug(slug),\n  };\n}\n\nexport async function loadProjectDocsCatalog(\n  root = findProjectRoot()\n): Promise<ProjectDocsCatalogEntry[]> {\n  const docsDirectory = path.join(root, \"docs/frontend\");\n\n  if (!existsSync(docsDirectory)) {\n    return [];\n  }\n\n  const entries = await readdir(docsDirectory);\n  const catalog: ProjectDocsCatalogEntry[] = [];\n\n  for (const entry of entries.sort()) {\n    if (!entry.endsWith(\".md\")) {\n      continue;\n    }\n\n    const slug = entry.slice(0, -3);\n\n    if (excludedFrontendDocSlugs.has(slug)) {\n      continue;\n    }\n\n    const absoluteDocsPath = path.join(docsDirectory, entry);\n    const metadata = await readDocsMetadata(absoluteDocsPath, slug);\n    const absoluteReadmePath = path.join(\n      root,\n      \"apps/web/features\",\n      slug,\n      \"README.md\"\n    );\n\n    catalog.push({\n      docsPath: toRepoPath(root, absoluteDocsPath),\n      href: `/demos/${slug}`,\n      pattern: inferPattern(slug),\n      readmePath: existsSync(absoluteReadmePath)\n        ? toRepoPath(root, absoluteReadmePath)\n        : undefined,\n      slug,\n      source: \"docs/frontend\",\n      status: inferStatus(slug),\n      summary: metadata.summary,\n      title: metadata.title,\n    });\n  }\n\n  return catalog.sort((left, right) => left.title.localeCompare(right.title));\n}\n\nasync function collectMarkdownPaths(root: string, directory: string) {\n  const absoluteDirectory = path.join(root, directory);\n\n  if (!existsSync(absoluteDirectory)) {\n    return [];\n  }\n\n  const entries = await readdir(absoluteDirectory, { withFileTypes: true });\n  const files: string[] = [];\n\n  for (const entry of entries) {\n    const relativePath = path.join(directory, entry.name);\n\n    if (entry.isDirectory()) {\n      files.push(...(await collectMarkdownPaths(root, relativePath)));\n      continue;\n    }\n\n    if (entry.isFile() && entry.name.endsWith(\".md\")) {\n      files.push(relativePath);\n    }\n  }\n\n  return files;\n}\n\nexport async function listProjectDocsSearchPaths(root = findProjectRoot()) {\n  const paths = new Set(await collectMarkdownPaths(root, \"docs\"));\n  const catalog = await loadProjectDocsCatalog(root);\n\n  for (const entry of catalog) {\n    if (entry.readmePath) {\n      paths.add(entry.readmePath);\n    }\n  }\n\n  return Array.from(paths).sort();\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/project-docs-catalog.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/project-mcp-server.ts",
      "content": "// biome-ignore lint/correctness/noUnresolvedImports: the MCP SDK wildcard export requires the .js subpath at runtime.\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\n// biome-ignore lint/correctness/noUnresolvedImports: the MCP SDK wildcard export requires the .js subpath at runtime.\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport { z } from \"zod\";\nimport {\n  projectDemoPatterns,\n  projectDemoStatuses,\n} from \"./project-docs-catalog\";\nimport {\n  listDemoCatalogForMcp,\n  readDemoDocsForMcp,\n  searchProjectDocsForMcp,\n} from \"./project-tools\";\n\nconst jsonText = (value: unknown) => ({\n  content: [\n    {\n      text: JSON.stringify(value, null, 2),\n      type: \"text\" as const,\n    },\n  ],\n});\n\nexport function createProjectDocsMcpServer() {\n  const server = new McpServer(\n    {\n      name: \"mcp-agent-project-docs\",\n      title: \"MCP Agent Project Docs\",\n      version: \"0.1.0\",\n    },\n    {\n      instructions:\n        \"Use these tools for repository docs, demo catalog, and AI SDK recipes checklist questions.\",\n    }\n  );\n\n  server.registerTool(\n    \"list_demos\",\n    {\n      description:\n        \"List demo catalog entries, optionally filtered by status or pattern.\",\n      inputSchema: {\n        pattern: z.enum(projectDemoPatterns).optional(),\n        status: z.enum(projectDemoStatuses).optional(),\n      },\n    },\n    async ({ pattern, status }) =>\n      jsonText(await listDemoCatalogForMcp({ pattern, status }))\n  );\n\n  server.registerTool(\n    \"read_demo_docs\",\n    {\n      description:\n        \"Read durable docs and the feature README for one demo slug when present.\",\n      inputSchema: {\n        slug: z.string().min(1),\n      },\n    },\n    async ({ slug }) => jsonText(await readDemoDocsForMcp({ slug }))\n  );\n\n  server.registerTool(\n    \"search_project_docs\",\n    {\n      description: \"Search durable project docs for line-level matches.\",\n      inputSchema: {\n        limit: z.number().int().min(1).max(30).optional(),\n        query: z.string().min(1),\n      },\n    },\n    async ({ limit, query }) =>\n      jsonText(await searchProjectDocsForMcp({ limit, query }))\n  );\n\n  return server;\n}\n\nexport async function handleProjectDocsMcpRequest(request: Request) {\n  const server = createProjectDocsMcpServer();\n  const transport = new WebStandardStreamableHTTPServerTransport({\n    enableJsonResponse: true,\n    sessionIdGenerator: undefined,\n  });\n\n  await server.connect(transport);\n\n  try {\n    return await transport.handleRequest(request);\n  } finally {\n    await server.close();\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/project-mcp-server.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/project-tools.ts",
      "content": "import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n  findProjectRoot,\n  listProjectDocsSearchPaths,\n  loadProjectDocsCatalog,\n  type ProjectDocsCatalogEntry,\n  type ProjectDemoPattern,\n  type ProjectDemoStatus,\n} from \"./project-docs-catalog\";\n\nexport interface ProjectDocFile {\n  content: string;\n  path: string;\n}\n\nexport interface DemoDocsBundle {\n  files: ProjectDocFile[];\n  meta: ProjectDocsCatalogEntry;\n  slug: string;\n}\n\nexport interface ProjectDocSearchMatch {\n  line: number;\n  path: string;\n  text: string;\n}\n\nexport const projectMcpToolDefinitions = [\n  {\n    description:\n      \"List demo catalog entries, optionally filtered by status or pattern.\",\n    name: \"list_demos\",\n  },\n  {\n    description:\n      \"Read the durable docs bundle for one demo slug, including docs/frontend and feature README when present.\",\n    name: \"read_demo_docs\",\n  },\n  {\n    description:\n      \"Search the project docs system for concise line-level matches.\",\n    name: \"search_project_docs\",\n  },\n] as const;\n\nasync function readRepoFile(\n  relativePath: string,\n  root = findProjectRoot()\n): Promise<ProjectDocFile | null> {\n  const absolutePath = path.join(root, relativePath);\n\n  if (!existsSync(absolutePath)) {\n    return null;\n  }\n\n  return {\n    content: await readFile(absolutePath, \"utf8\"),\n    path: path.relative(root, absolutePath),\n  };\n}\n\nexport async function listDemoCatalogForMcp({\n  pattern,\n  status,\n}: {\n  pattern?: ProjectDemoPattern;\n  status?: ProjectDemoStatus;\n} = {}) {\n  const catalog = await loadProjectDocsCatalog();\n\n  return catalog\n    .filter((entry) => (status ? entry.status === status : true))\n    .filter((entry) => (pattern ? entry.pattern === pattern : true))\n    .map((entry) => ({\n      href: entry.status === \"ready\" ? entry.href : undefined,\n      pattern: entry.pattern,\n      slug: entry.slug,\n      source: entry.source,\n      status: entry.status,\n      summary: entry.summary,\n      title: entry.title,\n    }));\n}\n\nexport async function readDemoDocsForMcp({\n  slug,\n}: {\n  slug: string;\n}): Promise<DemoDocsBundle> {\n  const root = findProjectRoot();\n  const catalog = await loadProjectDocsCatalog(root);\n  const meta = catalog.find((entry) => entry.slug === slug);\n\n  if (!meta) {\n    throw new Error(`Unknown demo slug: ${slug}`);\n  }\n\n  const candidatePaths = [meta.docsPath, meta.readmePath].filter(\n    (candidate): candidate is string => Boolean(candidate)\n  );\n  const files = (\n    await Promise.all(\n      candidatePaths.map((candidate) => readRepoFile(candidate, root))\n    )\n  ).filter((file): file is ProjectDocFile => Boolean(file));\n\n  if (files.length === 0) {\n    throw new Error(`No durable docs were found for demo slug: ${slug}`);\n  }\n\n  return {\n    files,\n    meta,\n    slug,\n  };\n}\n\nexport async function searchProjectDocsForMcp({\n  limit = 12,\n  query,\n}: {\n  limit?: number;\n  query: string;\n}) {\n  const normalizedQuery = query.trim().toLowerCase();\n\n  if (!normalizedQuery) {\n    throw new Error(\"Expected a non-empty docs search query.\");\n  }\n\n  const matches: ProjectDocSearchMatch[] = [];\n  const root = findProjectRoot();\n  const docsSearchPaths = await listProjectDocsSearchPaths(root);\n\n  for (const docsPath of docsSearchPaths) {\n    const file = await readRepoFile(docsPath, root);\n\n    if (!file) {\n      continue;\n    }\n\n    file.content.split(\"\\n\").forEach((line, index) => {\n      if (\n        matches.length < limit &&\n        line.toLowerCase().includes(normalizedQuery)\n      ) {\n        matches.push({\n          line: index + 1,\n          path: file.path,\n          text: line.trim(),\n        });\n      }\n    });\n  }\n\n  return {\n    matches,\n    query,\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/project-tools.ts"
    },
    {
      "path": "registry/mcp-agent/lib/mcp-agent/server/runtime.ts",
      "content": "import { type UIMessage, validateUIMessages } from \"ai\";\nimport { getMcpAgentEnv, getMcpAgentSetupState } from \"./env\";\nimport { streamMcpAgent } from \"./chat\";\nimport { projectMcpToolDefinitions } from \"./project-tools\";\n\ntype DemoEnv = Record<string, string | undefined>;\n\ninterface McpAgentRequestBody {\n  id?: string;\n  messages?: UIMessage[];\n}\n\ninterface StreamMcpAgentOptions {\n  origin: string;\n  sessionId: string;\n}\n\ninterface McpAgentRequestDependencies {\n  streamMcpAgent: (\n    messages: UIMessage[],\n    options: StreamMcpAgentOptions\n  ) => Promise<Response> | Response;\n}\n\nexport interface ConfiguredMcpServer {\n  description: string;\n  name: string;\n  requiredSetup: string;\n  transport: \"http\" | \"stdio\";\n}\n\nexport interface McpAgentRuntimeState {\n  chatModel: string;\n  configuredServers: ConfiguredMcpServer[];\n  configuredTools: { description: string; name: string; server: string }[];\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nconst invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\nconst invalidChatIdError =\n  'Expected a JSON body with an \"id\" string and a \"messages\" array.';\n\nexport const configuredMcpServers: ConfiguredMcpServer[] = [\n  {\n    description:\n      \"Repository-local MCP server for demo catalog, project docs, and checklist evidence.\",\n    name: \"project-docs\",\n    requiredSetup: \"Built in\",\n    transport: \"http\",\n  },\n  {\n    description:\n      \"Optional local Next.js development MCP server for route, log, and runtime diagnostics.\",\n    name: \"nextjs-runtime\",\n    requiredSetup: \"Run a Next.js dev server for the target app\",\n    transport: \"stdio\",\n  },\n];\n\nexport const configuredMcpTools = [\n  ...projectMcpToolDefinitions.map((tool) => ({\n    description: tool.description,\n    name: `project__${tool.name}`,\n    server: \"project-docs\",\n  })),\n  {\n    description:\n      \"Discovered from next-devtools-mcp when a compatible Next.js dev server is running.\",\n    name: \"nextjs__*\",\n    server: \"nextjs-runtime\",\n  },\n];\n\nasync function readMcpAgentRequest(\n  body: unknown\n): Promise<{ messages: UIMessage[]; sessionId: string }> {\n  const { id, messages } = (body ?? {}) as McpAgentRequestBody;\n\n  if (!(typeof id === \"string\" && id.length > 0 && Array.isArray(messages))) {\n    throw new Error(invalidChatIdError);\n  }\n\n  try {\n    return {\n      messages: await validateUIMessages({ messages }),\n      sessionId: id,\n    };\n  } catch {\n    throw new Error(invalidUiMessagesError);\n  }\n}\n\nexport function getMcpAgentRuntimeState(\n  env: DemoEnv = getMcpAgentEnv()\n): McpAgentRuntimeState {\n  const setup = getMcpAgentSetupState(env);\n  const issues = [...setup.issues];\n\n  return {\n    chatModel: setup.config.chatModel,\n    configuredServers: configuredMcpServers,\n    configuredTools: configuredMcpTools,\n    isChatAvailable: issues.length === 0,\n    nodeVersion: setup.nodeVersion,\n    setupMessage: issues.length > 0 ? issues.join(\" \") : null,\n    statusLabel: issues.length === 0 ? \"Ready\" : \"Setup required\",\n  };\n}\n\nexport async function handleMcpAgentRequest(\n  request: Request,\n  env: DemoEnv = getMcpAgentEnv(),\n  dependencies: Partial<McpAgentRequestDependencies> = {\n    streamMcpAgent: (messages, options) =>\n      streamMcpAgent(messages, {\n        env,\n        origin: options.origin,\n      }),\n  }\n) {\n  const streamAgent =\n    dependencies.streamMcpAgent ??\n    ((messages: UIMessage[], options: StreamMcpAgentOptions) =>\n      streamMcpAgent(messages, {\n        env,\n        origin: options.origin,\n      }));\n  const runtimeState = getMcpAgentRuntimeState(env);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const { messages, sessionId } = await readMcpAgentRequest(\n      await request.json()\n    );\n\n    return streamAgent(messages, {\n      origin: new URL(request.url).origin,\n      sessionId,\n    });\n  } catch (error) {\n    if (\n      error instanceof Error &&\n      [invalidUiMessagesError, invalidChatIdError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/mcp-agent/server/runtime.ts"
    },
    {
      "path": "registry/mcp-agent/docs/frontend/mcp-agent.md",
      "content": "---\ntitle: MCP Agent\ndescription: Stable source-core and UX conventions for the MCP Runtime Doctor Agent demo.\nupdateAt: 2026-05-24\n---\n\n# MCP Agent\n\n## Scope\n\n- Covers the shipped `mcp-agent` demo under `apps/web/features/mcp-agent`.\n- Covers MCP server discovery, tool naming, and the no-mode chat UX.\n\n## Domain Language\n\n- **Project Docs MCP**: Built-in HTTP MCP server that exposes repository docs, demo catalog metadata, and the AI SDK recipes checklist.\n- **Next.js Runtime MCP**: Optional stdio MCP server powered by `next-devtools-mcp` for local route, log, and runtime diagnostics.\n- **Namespaced MCP tool**: A tool exposed to the model with a server prefix such as `project__read_demo_docs` or `nextjs__get_errors`.\n\n## Current Subdomain Docs\n\n- Use `@ai-sdk/mcp` `createMCPClient()` as the source-core bridge between MCP servers and AI SDK tools.\n- Keep the first MCP agent as one normal chat workspace. Do not add a user-visible mode switch for Project Docs versus Next.js Runtime.\n- Let the user's message and suggestion copy naturally reveal which MCP server should be used.\n- Always connect the built-in Project Docs MCP server for out-of-box value.\n- Treat the Next.js Runtime MCP server as optional. Surface its unavailable status explicitly, then continue with available MCP tools.\n- Namespace all MCP tool names before handing them to the agent. Keep prefixes short and server-oriented: `project__*` and `nextjs__*`.\n- Inject the connected server summary and available tool names into the agent instructions on each call.\n- Close MCP clients when the stream finishes.\n- Keep runtime sidebar content declarative: configured MCP servers, configured tools, transport type, and setup expectations.\n- Project Docs MCP tools should read durable docs from `docs/` and feature-local `README.md` files rather than scraping rendered pages.\n- The first suggestions should cover both scenes without a mode label: project docs review, Next.js runtime diagnostics, and checklist-driven demo planning.\n\n## Update Triggers\n\n- Update this file when a new MCP server is added or removed from `mcp-agent`.\n- Update this file when MCP tool prefixes change.\n- Update this file when the runtime sidebar stops reflecting configured MCP servers and tools.\n",
      "type": "registry:file",
      "target": "docs/frontend/mcp-agent.md"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-5-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the MCP agent page, chat route, built-in project-docs MCP route, client workspace, local project-docs discovery, and the AI Gateway env vars required for MCP-backed chat. The optional Next.js runtime MCP server becomes available after next-devtools-mcp can attach to a running Next.js dev server.",
  "type": "registry:block"
}
