How SamChatAgent Works in OpenSEO: Architecture and Implementation Guide

SamChatAgent is the built-in SEO assistant in OpenSEO that orchestrates durable chat sessions, dynamic system prompts, and MCP tool adaptation to deliver real-time SEO analysis through a React-based interface.

OpenSEO ships with an in-app AI assistant called SAM (Search-Assistant-Machine), implemented as the SamChatAgent. This agent provides context-aware SEO research by combining persistent conversation state, carefully engineered system prompts, and secure access to the platform's full tool suite. Understanding how SamChatAgent functions requires examining its three-layer architecture spanning session management, prompt generation, and tool orchestration.

SamChatAgent Architecture Overview

The SamChatAgent consists of three tightly coupled layers that handle distinct responsibilities.

Session Management and Lifecycle

When a user navigates to /p/{projectId}/sam, the route defined in src/routes/_project/p/$projectId/sam.tsx renders the SamChat component. This React component initializes the conversation by querying existing sessions.

The component uses React-Query to call listSamSessions, which retrieves historical conversations from the SamSessionRepository. If no sessions exist, the UI presents an empty state with a "New chat" button. Clicking this triggers the createSamSession mutation:

// src/serverFunctions/sam.ts
export const createSamSession = createServerFn({ method: "POST" })
  .middleware(requireProjectContext)
  .validator(z.object({ projectId: z.string().min(1) }))
  .handler(async ({ context }) => {
    const session = await SamSessionRepository.createSession({
      projectId: context.projectId,
      userId: context.userId,
    });
    if (!session) {
      throw new AppError("INTERNAL_ERROR", "Failed to create chat session");
    }
    return { id: session.id };
  });

Upon successful creation, the UI navigates to the specific session via query parameter (?s={sessionId}), establishing the context for the durable object connection that powers real-time message streaming.

System Prompt Generation

Each conversation turn begins with buildSamSystemPrompt in src/server/features/sam/samSystemPrompt.ts constructing the instructions that guide the LLM's behavior. This function concatenates several distinct sections into a single string:

  • Static persona rules: Hard-coded instructions defining SAM as an SEO research assistant with concise response requirements
  • Credit-aware constraints: Rules preventing hallucination of metrics and enforcing disciplined tool usage
  • Project context block: Dynamic insertion of the active project name, domain, and market location from the shared memory
  • Intake mode instructions: Optional onboarding guidance when bootstrapping new projects
// src/server/features/sam/samSystemPrompt.ts
export function buildSamSystemPrompt(
  project: SamProjectContext,
  options: { intakeMode: boolean },
): string {
  const market = LOCATIONS[project.locationCode] ?? "the project's market";
  const sections = [
    "You are SAM, the SEO agent inside OpenSEO…",
    `Active project: "${project.projectName}" (projectId: ${project.projectId}).`,
    project.domain
      ? `Project website: ${project.domain}. Default market: ${market} …`
      : `This project has no website set yet…`,
  ];
  if (options.intakeMode) sections.push(/* onboarding instructions */);
  return sections.join("\n\n");
}

MCP Tool Adaptation and Security

SAM's capabilities surface through the MCP tool set, adapted for the AI SDK in src/server/features/sam/samChatTools.ts. The buildSamMcpTools function performs several critical transformations to ensure security and functionality.

Project Isolation: The adaptMcpTool function strips the projectId field from the model-facing schema, then re-injects it server-side during execution. This guarantees the LLM cannot access data outside the current project scope:

// src/server/features/sam/samChatTools.ts
function adaptMcpTool<Shape extends ZodRawShape>(
  def: McpToolDefinition<Shape>,
  context: ToolContext,
  projectId: string,
): Tool {
  const { projectId: _projectIdSchema, ...modelShape } = def.config.inputSchema;
  const bindsProject = "projectId" in def.config.inputSchema;
  
  return tool({
    description: def.config.description,
    inputSchema: z.object(bindsProject ? modelShape : def.config.inputSchema),
    execute: async (args) => {
      const fullArgs = (bindsProject ? { ...args, projectId } : args);
      // Execute with server-side project context
      return toModelOutput(await withPgClient(() => handler(fullArgs, context)));
    },
  });
}

Instrumentation: Each tool wraps with instrumentMcpToolHandler to enforce unified credit metering, authentication checks, and telemetry emission (mcp:tool_call) identical to the external API surface.

Free Utilities: The agent includes complimentary site-reading tools (map_links, read_pages) that allow SAM to discover and analyze pages without consuming user credits, alongside product information access via get_product_info.

Skills Catalog Integration

Public skills are defined as markdown files under .agents/skills/*/SKILL.md. The buildSamSkillSource function in src/server/features/sam/samSkills.ts dynamically imports these files using Vite's import.meta.glob:

// src/server/features/sam/samSkills.ts
const skillFiles = import.meta.glob('.agents/skills/*/SKILL.md', { eager: true });

The function parses each manifest's frontmatter using yaml and zod validation, filters internal tooling, and returns a SkillSource interface. Skills meant for the SAM surface receive preprocessing that removes steps irrelevant to the in-app context, such as "verify MCP connection" instructions.

Access Control and API Key Gating

Because SAM relies on the OpenRouter LLM, the platform verifies API key availability before allowing chat initialization. The getSamAccessSetupStatus function in src/serverFunctions/samAccess.ts checks whether the deployment is hosted (keys always present) or self-hosted (keys optional):

// src/serverFunctions/samAccess.ts
export async function getSamAccessSetupStatus(projectId: string) {
  // Hosted deployments always have keys; self-hosted checks env
  const hasKey = process.env.OPENROUTER_API_KEY || isHosted();
  return { 
    enabled: hasKey,
    setupRequired: !hasKey 
  };
}

If the key is missing, the SamSetupGate component renders instead of the chat interface, preventing failed API calls.

End-to-End Message Flow

The complete interaction follows this sequence:

  1. Route Activation: User accesses /p/{projectId}/sam, triggering the route handler in src/routes/_project/p/$projectId/sam.tsx.
  2. Session Initialization: SamChat queries listSamSessions and auto-selects the newest session or prompts for creation.
  3. Prompt Construction: The backend calls buildSamSystemPrompt with the current project context and intake mode flag.
  4. Tool Preparation: buildSamMcpTools adapts the full MCP suite, injecting the project ID and wrapping handlers for metering.
  5. LLM Interaction: The AI SDK sends the prompt and tool definitions; the model may invoke adapted tools like get_domain_overview or map_links.
  6. Execution and Response: Tool calls execute with server-side project context, credits deduct automatically, and results flatten via toModelOutput before returning to the model for synthesis.

Summary

  • SamChatAgent operates through a three-layer architecture separating session persistence, prompt generation, and UI rendering concerns in the open-seo codebase.
  • Session management relies on SamSessionRepository and React-Query hooks in SamChat.tsx to maintain durable conversation state.
  • System prompts combine static personality rules with dynamic project context through buildSamSystemPrompt, enforcing credit-aware behavior.
  • Security isolation occurs via adaptMcpTool, which removes projectId from the LLM's view while injecting it server-side during execution.
  • Skills loading uses Vite's import.meta.glob to dynamically import markdown skill files from .agents/skills/.
  • Access control gates the feature behind getSamAccessSetupStatus checks for the OPENROUTER_API_KEY environment variable.

Frequently Asked Questions

What is SamChatAgent in OpenSEO?

SamChatAgent is the embedded AI assistant (SAM) that provides SEO research and recommendations directly within the OpenSEO dashboard. According to the open-seo source code, it functions as a conversational interface to the platform's MCP tools, allowing users to analyze domains, map site structures, and receive optimization guidance through a chat interface powered by OpenRouter LLMs.

How does SamChatAgent prevent cross-project data access?

The agent enforces project isolation through schema manipulation in the adaptMcpTool function. This utility strips the projectId parameter from the Zod schema exposed to the language model, then binds the server-side project ID during tool execution. As implemented in src/server/features/sam/samChatTools.ts, this ensures the LLM can only access data from the currently active project regardless of prompt injection attempts.

What tools are available to SamChatAgent?

SAM has access to the full OpenSEO MCP tool suite plus specialized utilities. According to samChatTools.ts, these include credit-metered research tools (adapted from MCP definitions), free site-reading utilities (map_links, read_pages), and product information access (get_product_info). All tools wrap with instrumentMcpToolHandler for unified telemetry and billing.

How do I enable SamChatAgent in a self-hosted OpenSEO instance?

You must provide an OPENROUTER_API_KEY environment variable. The getSamAccessSetupStatus function in samAccess.ts checks for this key before allowing chat initialization. Self-hosted deployments without this key display the SamSetupGate component, which blocks the chat interface until the API key is configured.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →