# How SamChatAgent Works in OpenSEO: Architecture and Implementation Guide

> Discover how SamChatAgent works in OpenSEO. Learn about its architecture, implementation, and how it provides real-time SEO analysis with chat sessions, prompts, and tool adaptation.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: architecture
- Published: 2026-09-10

---

**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 & API Layer**: Manages chat lifecycle through `createSamSession`, `listSamSessions`, and `archiveSamSession` in [`src/serverFunctions/sam.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/sam.ts). This layer persists conversation state and establishes durable connections for real-time messaging.
- **Agent Core**: Generates the system prompt that defines SAM's personality and adapts every MCP tool into AI-SDK compatible functions. Key files include [`src/server/features/sam/samSystemPrompt.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/samSystemPrompt.ts) for prompt construction and [`src/server/features/sam/samChatTools.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/samChatTools.ts) for tool wrapping.
- **Skill & UI Layer**: Bundles the public skill catalog via [`src/server/features/sam/samSkills.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/samSkills.ts) and renders the interface through [`src/client/features/sam/SamChat.tsx`](https://github.com/every-app/open-seo/blob/main/src/client/features/sam/SamChat.tsx), while gating access through [`src/serverFunctions/samAccess.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/samAccess.ts).

## 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:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/src/server/features/sam/samSkills.ts) dynamically imports these files using Vite's `import.meta.glob`:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/samAccess.ts) checks whether the deployment is hosted (keys always present) or self-hosted (keys optional):

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.