From 93ee343c631377aa959f1bd4fd55a86f49fc8492 Mon Sep 17 00:00:00 2001 From: Ben Senescu Date: Fri, 8 May 2026 14:56:26 -0400 Subject: [PATCH] Polish AI & MCP page and move out of project scope - Move /p/$projectId/ai to /ai so the page can be linked directly - Lead with a copyable MCP server URL and trim the surrounding copy - Make Claude Code and Codex setup guides collapsible accordions - Add an Available tools section grouped into Keywords and Domain --- src/client/features/ai-mcp/AvailableTools.tsx | 89 +++++ src/client/navigation/items.ts | 15 +- src/routeTree.gen.ts | 42 +-- src/routes/_app/ai.tsx | 286 ++++++++++++++++ src/routes/_project/p/$projectId/ai.tsx | 323 ------------------ 5 files changed, 404 insertions(+), 351 deletions(-) create mode 100644 src/client/features/ai-mcp/AvailableTools.tsx create mode 100644 src/routes/_app/ai.tsx delete mode 100644 src/routes/_project/p/$projectId/ai.tsx diff --git a/src/client/features/ai-mcp/AvailableTools.tsx b/src/client/features/ai-mcp/AvailableTools.tsx new file mode 100644 index 0000000..58663a7 --- /dev/null +++ b/src/client/features/ai-mcp/AvailableTools.tsx @@ -0,0 +1,89 @@ +type McpTool = { + name: string; + title: string; + description: string; +}; + +type ToolCategory = { + label: string; + tools: McpTool[]; +}; + +const toolCategories: ToolCategory[] = [ + { + label: "Keywords", + tools: [ + { + name: "research_keywords", + title: "Research keywords", + description: "Get keyword ideas with volume, difficulty, and CPC.", + }, + { + name: "get_serp_results", + title: "Get SERP results", + description: "See live Google results for a keyword.", + }, + { + name: "get_rank_tracker", + title: "Get rank tracking positions", + description: "Read tracked keyword positions.", + }, + { + name: "list_saved_keywords", + title: "Get saved keywords", + description: "Pull your saved keyword lists.", + }, + { + name: "save_keywords", + title: "Save keywords", + description: "Save keywords back to OpenSEO.", + }, + ], + }, + { + label: "Domain", + tools: [ + { + name: "get_domain_overview", + title: "Get domain overview", + description: "Summarize a domain's organic footprint.", + }, + { + name: "get_domain_keyword_suggestions", + title: "Get domain keywords", + description: "Find keywords a domain already ranks for.", + }, + { + name: "get_backlinks_overview", + title: "Get backlinks overview", + description: "Check backlink and referring-domain stats.", + }, + ], + }, +]; + +export function AvailableTools() { + return ( +
+ {toolCategories.map((cat) => ( +
+

+ {cat.label} +

+
    + {cat.tools.map((tool) => ( +
  • + + {tool.title} + +

    + {tool.description} +

    +
  • + ))} +
+
+ ))} +
+ ); +} diff --git a/src/client/navigation/items.ts b/src/client/navigation/items.ts index 643378d..9635dff 100644 --- a/src/client/navigation/items.ts +++ b/src/client/navigation/items.ts @@ -60,14 +60,15 @@ const projectNavItems = [ icon: MessageSquare, matchSegment: "/prompt-explorer", }, - { - to: "/p/$projectId/ai" as const, - label: "AI & MCP", - icon: Bot, - matchSegment: "/ai", - }, ] as const; +const aiNavItem = linkOptions({ + to: "/ai" as const, + label: "AI & MCP", + icon: Bot, + matchSegment: "/ai", +}); + function getProjectNavItems(projectId: string) { return linkOptions( projectNavItems.map((item) => ({ @@ -114,7 +115,7 @@ export function getProjectNavGroups(projectId: string) { }, { type: "standalone" as const, - item: bySegment("/ai"), + item: aiNavItem, }, ]; } diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 7ac9750..7a9e9d6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -24,6 +24,7 @@ import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in' import { Route as AppSupportRouteImport } from './routes/_app/support' import { Route as AppSettingsRouteImport } from './routes/_app/settings' import { Route as AppBillingRouteImport } from './routes/_app/billing' +import { Route as AppAiRouteImport } from './routes/_app/ai' import { Route as DotwellKnownOpenidConfigurationRouteImport } from './routes/[.]well-known/openid-configuration' import { Route as DotwellKnownOauthAuthorizationServerRouteImport } from './routes/[.]well-known/oauth-authorization-server' import { Route as ApiAutumnSplatRouteImport } from './routes/api/autumn/$' @@ -40,7 +41,6 @@ import { Route as ProjectPProjectIdDomainRouteImport } from './routes/_project/p import { Route as ProjectPProjectIdBrandLookupRouteImport } from './routes/_project/p/$projectId/brand-lookup' import { Route as ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks' import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit' -import { Route as ProjectPProjectIdAiRouteImport } from './routes/_project/p/$projectId/ai' import { Route as DotwellKnownOauthAuthorizationServerApiAuthRouteImport } from './routes/[.]well-known/oauth-authorization-server/api/auth' import { Route as ProjectPProjectIdRankTrackingIndexRouteImport } from './routes/_project/p/$projectId/rank-tracking/index' import { Route as ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index' @@ -119,6 +119,11 @@ const AppBillingRoute = AppBillingRouteImport.update({ path: '/billing', getParentRoute: () => AppRouteRoute, } as any) +const AppAiRoute = AppAiRouteImport.update({ + id: '/ai', + path: '/ai', + getParentRoute: () => AppRouteRoute, +} as any) const DotwellKnownOpenidConfigurationRoute = DotwellKnownOpenidConfigurationRouteImport.update({ id: '/.well-known/openid-configuration', @@ -207,11 +212,6 @@ const ProjectPProjectIdAuditRoute = ProjectPProjectIdAuditRouteImport.update({ path: '/audit', getParentRoute: () => ProjectPProjectIdRouteRoute, } as any) -const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({ - id: '/ai', - path: '/ai', - getParentRoute: () => ProjectPProjectIdRouteRoute, -} as any) const DotwellKnownOauthAuthorizationServerApiAuthRoute = DotwellKnownOauthAuthorizationServerApiAuthRouteImport.update({ id: '/api/auth', @@ -250,6 +250,7 @@ export interface FileRoutesByFullPath { '/verify-email': typeof VerifyEmailRoute '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute + '/ai': typeof AppAiRoute '/billing': typeof AppBillingRoute '/settings': typeof AppSettingsRoute '/support': typeof AppSupportRoute @@ -263,7 +264,6 @@ export interface FileRoutesByFullPath { '/api/auth/$': typeof ApiAuthSplatRoute '/api/autumn/$': typeof ApiAutumnSplatRoute '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute - '/p/$projectId/ai': typeof ProjectPProjectIdAiRoute '/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren '/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute '/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute @@ -285,6 +285,7 @@ export interface FileRoutesByTo { '/verify-email': typeof VerifyEmailRoute '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute + '/ai': typeof AppAiRoute '/billing': typeof AppBillingRoute '/settings': typeof AppSettingsRoute '/support': typeof AppSupportRoute @@ -297,7 +298,6 @@ export interface FileRoutesByTo { '/api/auth/$': typeof ApiAuthSplatRoute '/api/autumn/$': typeof ApiAutumnSplatRoute '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute - '/p/$projectId/ai': typeof ProjectPProjectIdAiRoute '/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute '/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute '/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute @@ -321,6 +321,7 @@ export interface FileRoutesById { '/verify-email': typeof VerifyEmailRoute '/.well-known/oauth-authorization-server': typeof DotwellKnownOauthAuthorizationServerRouteWithChildren '/.well-known/openid-configuration': typeof DotwellKnownOpenidConfigurationRoute + '/_app/ai': typeof AppAiRoute '/_app/billing': typeof AppBillingRoute '/_app/settings': typeof AppSettingsRoute '/_app/support': typeof AppSupportRoute @@ -335,7 +336,6 @@ export interface FileRoutesById { '/api/auth/$': typeof ApiAuthSplatRoute '/api/autumn/$': typeof ApiAutumnSplatRoute '/.well-known/oauth-authorization-server/api/auth': typeof DotwellKnownOauthAuthorizationServerApiAuthRoute - '/_project/p/$projectId/ai': typeof ProjectPProjectIdAiRoute '/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren '/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute '/_project/p/$projectId/brand-lookup': typeof ProjectPProjectIdBrandLookupRoute @@ -359,6 +359,7 @@ export interface FileRouteTypes { | '/verify-email' | '/.well-known/oauth-authorization-server' | '/.well-known/openid-configuration' + | '/ai' | '/billing' | '/settings' | '/support' @@ -372,7 +373,6 @@ export interface FileRouteTypes { | '/api/auth/$' | '/api/autumn/$' | '/.well-known/oauth-authorization-server/api/auth' - | '/p/$projectId/ai' | '/p/$projectId/audit' | '/p/$projectId/backlinks' | '/p/$projectId/brand-lookup' @@ -394,6 +394,7 @@ export interface FileRouteTypes { | '/verify-email' | '/.well-known/oauth-authorization-server' | '/.well-known/openid-configuration' + | '/ai' | '/billing' | '/settings' | '/support' @@ -406,7 +407,6 @@ export interface FileRouteTypes { | '/api/auth/$' | '/api/autumn/$' | '/.well-known/oauth-authorization-server/api/auth' - | '/p/$projectId/ai' | '/p/$projectId/backlinks' | '/p/$projectId/brand-lookup' | '/p/$projectId/domain' @@ -429,6 +429,7 @@ export interface FileRouteTypes { | '/verify-email' | '/.well-known/oauth-authorization-server' | '/.well-known/openid-configuration' + | '/_app/ai' | '/_app/billing' | '/_app/settings' | '/_app/support' @@ -443,7 +444,6 @@ export interface FileRouteTypes { | '/api/auth/$' | '/api/autumn/$' | '/.well-known/oauth-authorization-server/api/auth' - | '/_project/p/$projectId/ai' | '/_project/p/$projectId/audit' | '/_project/p/$projectId/backlinks' | '/_project/p/$projectId/brand-lookup' @@ -581,6 +581,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppBillingRouteImport parentRoute: typeof AppRouteRoute } + '/_app/ai': { + id: '/_app/ai' + path: '/ai' + fullPath: '/ai' + preLoaderRoute: typeof AppAiRouteImport + parentRoute: typeof AppRouteRoute + } '/.well-known/openid-configuration': { id: '/.well-known/openid-configuration' path: '/.well-known/openid-configuration' @@ -693,13 +700,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectPProjectIdAuditRouteImport parentRoute: typeof ProjectPProjectIdRouteRoute } - '/_project/p/$projectId/ai': { - id: '/_project/p/$projectId/ai' - path: '/ai' - fullPath: '/p/$projectId/ai' - preLoaderRoute: typeof ProjectPProjectIdAiRouteImport - parentRoute: typeof ProjectPProjectIdRouteRoute - } '/.well-known/oauth-authorization-server/api/auth': { id: '/.well-known/oauth-authorization-server/api/auth' path: '/api/auth' @@ -739,6 +739,7 @@ declare module '@tanstack/react-router' { } interface AppRouteRouteChildren { + AppAiRoute: typeof AppAiRoute AppBillingRoute: typeof AppBillingRoute AppSettingsRoute: typeof AppSettingsRoute AppSupportRoute: typeof AppSupportRoute @@ -747,6 +748,7 @@ interface AppRouteRouteChildren { } const AppRouteRouteChildren: AppRouteRouteChildren = { + AppAiRoute: AppAiRoute, AppBillingRoute: AppBillingRoute, AppSettingsRoute: AppSettingsRoute, AppSupportRoute: AppSupportRoute, @@ -794,7 +796,6 @@ const ProjectPProjectIdRankTrackingRouteWithChildren = ) interface ProjectPProjectIdRouteRouteChildren { - ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute ProjectPProjectIdBrandLookupRoute: typeof ProjectPProjectIdBrandLookupRoute @@ -808,7 +809,6 @@ interface ProjectPProjectIdRouteRouteChildren { const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren = { - ProjectPProjectIdAiRoute: ProjectPProjectIdAiRoute, ProjectPProjectIdAuditRoute: ProjectPProjectIdAuditRouteWithChildren, ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute, ProjectPProjectIdBrandLookupRoute: ProjectPProjectIdBrandLookupRoute, diff --git a/src/routes/_app/ai.tsx b/src/routes/_app/ai.tsx new file mode 100644 index 0000000..94a86b1 --- /dev/null +++ b/src/routes/_app/ai.tsx @@ -0,0 +1,286 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ArrowUpRight, Check, ChevronDown, Copy } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { AvailableTools } from "@/client/features/ai-mcp/AvailableTools"; + +const DISCORD_URL = "https://discord.gg/c9uGs3cFXr"; +const SUPPORT_EMAIL = "ben@openseo.so"; +const SAM_GITHUB_URL = "https://github.com/every-app/sam"; + +export const Route = createFileRoute("/_app/ai")({ + component: AiPage, +}); + +function AiPage() { + const mcpUrl = + typeof window === "undefined" + ? "https://app.openseo.so/mcp" + : `${window.location.origin}/mcp`; + + return ( +
+
+

AI & MCP

+

+ Connect your AI agent to OpenSEO. Run keyword research, SERP analysis, + and domain lookups from your editor or chat. +

+ +
+
+
+

+ MCP server URL +

+ +
+ + {mcpUrl} + +
+

+ Paste this into any MCP client. Sign in with OpenSEO when prompted. +

+
+ +
+

Setup guides

+

+ Pick your agent. +

+
+ +

+ Run this in your terminal: +

+ +

+ Approve the login when prompted. Then ask Claude Code to call{" "} + whoami. +

+
+ + +

+ Run these commands: +

+ +

+ Approve the login when prompted. Then ask Codex to call{" "} + whoami. +

+
+
+
+ +
+

Available tools

+
+ +
+
+ +
+

Sam: AI SEO teammate

+

+ Sam is an experimental content workflow for Claude Code and other + coding agents. It combines keyword research, source discovery, + drafting, and QA. +

+ + View Sam on GitHub + + +
+ +
+

Roadmap

+
    + {[ + { + title: "In-app SEO Research Agent", + description: + "Ask questions and run research without leaving OpenSEO", + }, + { + title: "Content Assistant", + description: + "Generate drafts using saved keywords and business context", + }, + ].map((item) => ( +
  • + + — + + + + {item.title} + +
    + {item.description} +
    +
  • + ))} +
+
+ +

+ Have feedback? Reach out on{" "} + + Discord + {" "} + or email{" "} + + {SUPPORT_EMAIL} + + . +

+
+
+ ); +} + +function Collapsible({ + id, + title, + subtitle, + children, +}: { + id: string; + title: string; + subtitle?: string; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(false); + const contentId = `collapsible-${id}`; + + return ( +
+ + {open ? ( +
+ {children} +
+ ) : null} +
+ ); +} + +function CodeBlock({ code }: { code: string }) { + return ( +
+
+        {code}
+      
+
+ +
+
+ ); +} + +function CopyButton({ + value, + successMessage, + label, + iconOnly = false, +}: { + value: string; + successMessage: string; + label?: string; + iconOnly?: boolean; +}) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) { + toast.error("Clipboard not available"); + return; + } + try { + await navigator.clipboard.writeText(value); + toast.success(successMessage); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error("Could not copy to clipboard"); + } + }; + + if (iconOnly) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/routes/_project/p/$projectId/ai.tsx b/src/routes/_project/p/$projectId/ai.tsx deleted file mode 100644 index 2802322..0000000 --- a/src/routes/_project/p/$projectId/ai.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { ArrowUpRight } from "lucide-react"; - -const DISCORD_URL = "https://discord.gg/c9uGs3cFXr"; -const SUPPORT_EMAIL = "ben@openseo.so"; -const SAM_GITHUB_URL = "https://github.com/every-app/sam"; -const CHATGPT_MCP_DOCS_URL = "https://platform.openai.com/docs/mcp"; -const CLAUDE_CONNECTORS_DOCS_URL = - "https://support.anthropic.com/en/articles/11503834-building-custom-integrations-via-remote-mcp-servers"; - -const mcpTools = [ - { - name: "whoami", - description: - "Check which OpenSEO user and workspace the client can access.", - }, - { - name: "list_projects", - description: "Find your OpenSEO projects and copy the right projectId.", - }, - { - name: "research_keywords", - description: - "Turn seed terms into keyword ideas with volume, difficulty, and CPC.", - }, - { - name: "get_serp_results", - description: - "Inspect live Google results for keyword and competitor research.", - }, - { - name: "get_domain_overview", - description: - "Summarize a domain's organic footprint before deeper research.", - }, - { - name: "get_domain_keyword_suggestions", - description: - "Find keywords a domain already ranks for or could expand into.", - }, - { - name: "get_backlinks_overview", - description: "Review backlink authority and referring-domain signals.", - }, - { - name: "get_rank_tracker", - description: "Read your tracked keyword positions from OpenSEO.", - }, - { - name: "list_saved_keywords", - description: "Pull saved keyword lists into an agent workflow.", - }, - { - name: "save_keywords", - description: "Save promising keyword ideas back to OpenSEO.", - }, -] as const; - -const promptExamples = [ - { - title: "Keyword research", - prompt: - "Use OpenSEO to research keywords for my project. Start with these seed topics: [seed 1], [seed 2], [seed 3]. Group the best opportunities by search intent, prioritize low-difficulty terms, and suggest which keywords I should save.", - }, - { - title: "Competitor research", - prompt: - "Use OpenSEO to compare my domain [yourdomain.com] against these competitors: [competitor1.com], [competitor2.com]. Summarize their strongest keyword themes, backlink advantages, and the first content opportunities I should pursue.", - }, - { - title: "SERP brief", - prompt: - "Use OpenSEO to inspect the live SERP for [keyword]. Tell me who ranks, what search intent Google is rewarding, what pages are cited repeatedly, and what a better page should include.", - }, -] as const; - -export const Route = createFileRoute("/_project/p/$projectId/ai")({ - component: AiPage, -}); - -function AiPage() { - const mcpUrl = - typeof window === "undefined" - ? "https://app.openseo.so/mcp" - : `${window.location.origin}/mcp`; - - return ( -
-
-

AI & MCP

-

- Connect OpenSEO to your coding agent or AI workspace so it can run SEO - research with your project data: keyword ideas, SERP results, domain - overviews, backlinks, rank tracking, and saved keywords. -

-

- This is community-driven — we want to hear what matters to you. Reach - out on{" "} - - Discord - {" "} - or email{" "} - - {SUPPORT_EMAIL} - - . -

- -
-

MCP server

-

- Use this endpoint when a client asks for the OpenSEO MCP server URL. - You will be sent through OpenSEO login the first time a client - connects. -

-
-

- MCP endpoint -

- - {mcpUrl} - -
-
- -
-

Connect Codex

-

- Codex supports hosted MCP servers from the CLI. Add OpenSEO, then - run the OAuth login flow. -

-
-            {`codex mcp add openseo --url ${mcpUrl}
-codex mcp login openseo`}
-          
-

- After login, ask Codex to call whoami, then{" "} - list_projects. Use the returned projectId{" "} - for keyword, SERP, domain, backlink, and rank-tracking requests. -

-
- -
-

Connect Claude Code CLI

-

- Claude Code can add OpenSEO as an HTTP MCP server. Use the user - scope if you want it available across projects, or local scope if - this repository is the only place you need it. -

-
-            {`claude mcp add --transport http --scope user openseo ${mcpUrl}`}
-          
-

- If Claude Code prompts you to authorize the server, approve the - OpenSEO login flow. Then ask it to list MCP tools or call{" "} - whoami to confirm the connection. -

-
- -
-

- ChatGPT, Claude web, and Claude Desktop -

-

- Web AI clients handle MCP setup differently from coding tools. Use - the same endpoint above when adding a custom connector or MCP - server, then authorize OpenSEO when the client asks you to sign in. -

-
    -
  • - - — - - - In ChatGPT, enable developer mode if needed, then create a - custom MCP connector from Apps & Connectors and use the endpoint - above. - -
  • -
  • - - — - - - In Claude web or Claude Desktop, add OpenSEO from Settings, - Connectors. Remote MCP servers should be added there, not by - editing the desktop JSON config. - -
  • -
-

- Start by asking the client to check whoami and{" "} - list_projects. Once it can see your projects, it can - help with research planning, SERP analysis, competitor comparisons, - and saving useful keywords back to OpenSEO. -

-
- - ChatGPT MCP docs - - - - Claude connector docs - - -
-
- -
-

Available tools

-
- {mcpTools.map((tool) => ( -
- - {tool.name} - -

- {tool.description} -

-
- ))} -
-
- -
-

Prompts to try

-
- {promptExamples.map((example) => ( -
-

{example.title}

-

- {example.prompt} -

-
- ))} -
-
- -
-

Sam: AI SEO teammate

-

- Sam is an experimental content workflow for Claude Code and other - coding agents. It combines keyword research, source discovery, - drafting, and QA for teams that want an agent to help produce - publishable SEO content. -

- - View Sam on GitHub - - -
- -
-

Roadmap

-
    - {[ - { - title: "In-app SEO Research Agent", - description: - "Ask questions and run research without leaving OpenSEO", - }, - { - title: "Content Assistant", - description: - "Generate drafts using saved keywords and business context", - }, - { - title: "More MCP clients", - description: - "Better setup guidance for ChatGPT, Claude web, OpenCode, and other clients as their MCP support matures", - }, - ].map((item) => ( -
  • -

    - - — - - - - {item.title} - - {item.description ? ( - <> -
    - {item.description} - - ) : null} -
    -

    -
  • - ))} -
-
-
-
- ); -}