diff --git a/src/client/features/ai-mcp/AgentIcons.tsx b/src/client/features/ai-mcp/AgentIcons.tsx new file mode 100644 index 0000000..d61537c --- /dev/null +++ b/src/client/features/ai-mcp/AgentIcons.tsx @@ -0,0 +1,34 @@ +import type { SVGProps } from "react"; + +export function ClaudeIcon(props: SVGProps) { + return ( + + ); +} + +export function CodexIcon(props: SVGProps) { + return ( + + ); +} 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 20068d2..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 & Agents", - 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..a973ea5 --- /dev/null +++ b/src/routes/_app/ai.tsx @@ -0,0 +1,352 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { ArrowUpRight, Check, ChevronDown, Copy } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { ClaudeIcon, CodexIcon } from "@/client/features/ai-mcp/AgentIcons"; +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. +

+
+ + } + > +
    +
  1. + Open Settings →{" "} + Connectors. +
  2. +
  3. + Click{" "} + + Add custom connector + + . +
  4. +
  5. Paste the MCP URL above and click Add.
  6. +
  7. Approve the OpenSEO login when prompted.
  8. +
+

+ Requires a Claude Pro, Max, Team, or Enterprise plan. +

+
+ + } + > +

+ Run this in your terminal: +

+ +

+ Approve the login when prompted. +

+
+ + } + > +
    +
  1. + Open{" "} + + Settings → Integrations & MCP + + . +
  2. +
  3. + Click{" "} + + Add your own + + . +
  4. +
  5. Paste the MCP URL above.
  6. +
  7. Approve the OpenSEO login when prompted.
  8. +
+
+
+
+ +
+

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, + icon, + children, +}: { + id: string; + title: string; + subtitle?: string; + icon?: React.ReactNode; + 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 0aa141d..0000000 --- a/src/routes/_project/p/$projectId/ai.tsx +++ /dev/null @@ -1,158 +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 DATAFORSEO_MCP_DOCS_URL = - "https://dataforseo.com/help-center/setting-up-the-official-dataforseo-mcp-server-simple-guide"; - -export const Route = createFileRoute("/_project/p/$projectId/ai")({ - component: AiPage, -}); - -function AiPage() { - return ( -
-
-

AI & Agents

-

- OpenSEO is brand new. Our goal is to build the most powerful and - intuitive tool to help your websites rank better. AI will play a large - role in that. -

-

- Lots of other SEO tools have an overwhelming number of - “features”. We think many of these can be hidden by - default, only shown to you when you want them. -

-

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

- - {/* Sam */} -
-

Sam: AI SEO Teammate

-

- Sam turns your Claude Code or favorite coding agent into a powerful - SEO teammate. Sam has a specialized workflow for writing great - content, but you can teach Sam about your SEO workflow and needs and - it will customize itself for you. -

-

- Sam handles keyword research, source discovery, and drafting — with - a built-in QA loop that checks quality and accuracy before you - publish. -

-

- Works with DataForSEO MCP for real SEO data. Model-agnostic — use it - with Claude Code, GPT, or others. -

-
    - {[ - "Keyword research & topic analysis", - "Source discovery & verification", - "Content drafting with QA", - ].map((item) => ( -
  • - - — - - {item} -
  • - ))} -
- - View on GitHub - - -
- - {/* Use any coding agent */} -
-

Use any coding agent

-

- Pair your preferred agent with the DataForSEO MCP server to access - keyword volumes, SERP results, backlink profiles, and more. -

-

- Claude Code · Cursor · Windsurf · Codex - · any MCP-compatible agent -

- - DataForSEO MCP setup guide - - -
- - {/* Roadmap */} -
-

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 integrations", - }, - ].map((item) => ( -
  • -

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

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