# How to Save Keywords Using the OpenSEO MCP: A Complete Developer Guide

> Learn to save keywords using OpenSEO MCP. Developers can easily persist keyword data to the saved_keyword table via the saveKeywords RPC method. Full guide included.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-09-05

---

**Use the `saveKeywords` RPC method on the MCP endpoint at `https://app.openseo.so/mcp` with a valid Bearer token and JSON payload containing your keyword array to persist data to the `saved_keyword` table.**

The **every-app/open-seo** repository exposes a **Multi-Client Protocol (MCP)** that enables AI agents and external applications to programmatically manage SEO data. When you need to **save keywords using the OpenSEO MCP**, you interact with an RPC-style endpoint that handles authentication, validation via Zod, and database persistence through a layered service architecture.

## MCP Architecture and Authentication Flow

Before invoking any write operations, your client must obtain a short-lived MCP token. According to the authentication flow documented in [`web/content/docs/mcp.md`](https://github.com/every-app/open-seo/blob/main/web/content/docs/mcp.md), this token encodes both user identity and `projectId` scope. Without the `projectId` scope, the server rejects calls with a `401 Unauthorized` error.

## The Complete Workflow to Save Keywords

The implementation follows a three-tier architecture defined in the codebase:

1. **Request Handling** – The MCP router in [`src/server/routes/mcp.ts`](https://github.com/every-app/open-seo/blob/main/src/server/routes/mcp.ts) receives POST requests and routes `saveKeywords` calls to the service layer.
2. **Business Logic** – [`src/server/services/keyword.service.ts`](https://github.com/every-app/open-seo/blob/main/src/server/services/keyword.service.ts) contains the `KeywordService.saveMany()` method, which validates payloads against the Zod schema and coordinates with the repository.
3. **Data Persistence** – [`src/server/repositories/savedKeywordRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/repositories/savedKeywordRepository.ts) executes an `INSERT … ON CONFLICT … DO UPDATE` operation against the `saved_keyword` table defined in [`drizzle/0015_huge_celestials.sql`](https://github.com/every-app/open-seo/blob/main/drizzle/0015_huge_celestials.sql).

### Required Payload Schema

The request body must conform to the Zod schema implemented in the service layer:

```typescript
const SaveKeywordSchema = z.object({
  keyword: z.string(),
  tags: z.array(z.string()).optional(),
  notes: z.string().optional(),
});

const SaveKeywordsPayload = z.object({
  keywords: z.array(SaveKeywordSchema),
});

```

## Implementation Examples

### Node.js and Fetch

```typescript
import fetch from "node-fetch";

const MCP_ENDPOINT = "https://app.openseo.so/mcp";
const MCP_TOKEN = "<YOUR_MCP_TOKEN>";
const PROJECT_ID = "<PROJECT_ID>";

async function saveKeywords(keywords) {
  const body = {
    method: "saveKeywords",
    params: { keywords },
  };

  const resp = await fetch(`${MCP_ENDPOINT}?projectId=${PROJECT_ID}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${MCP_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!resp.ok) throw new Error(`MCP error ${resp.status}`);
  const data = await resp.json();
  console.log("Saved IDs:", data);
}

saveKeywords([
  { keyword: "air-conditioner repair", tags: ["service"], notes: "high intent" },
  { keyword: "best HVAC contractors", tags: ["competitor"], notes: "" },
]);

```

### AI Agent Integration

When configuring an AI agent (Claude, Codex, etc.) with MCP capabilities, send the following JSON structure:

```json
{
  "method": "saveKeywords",
  "params": {
    "keywords": [
      { "keyword": "solar panel installers", "tags": ["lead"], "notes": "seasonal" },
      { "keyword": "energy rebate programs", "tags": ["research"] }
    ]
  }
}

```

The agent must include the MCP token in the Authorization header as configured in its client settings.

### Retrieving Saved Keywords

To verify persistence or populate UI components, call `listSavedKeywords`:

```typescript
async function listSavedKeywords() {
  const resp = await fetch(`${MCP_ENDPOINT}?projectId=${PROJECT_ID}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${MCP_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ method: "listSavedKeywords", params: {} }),
  });

  const { keywords } = await resp.json();
  console.table(keywords);
}

```

## Database Schema and Idempotency

The `saved_keyword` table schema in [`drizzle/0015_huge_celestials.sql`](https://github.com/every-app/open-seo/blob/main/drizzle/0015_huge_celestials.sql) stores tags as JSON arrays and supports upsert semantics. When you **save keywords using the OpenSEO MCP** with a keyword that already exists, the repository updates the existing record's tags and notes rather than creating duplicates. This ensures idempotent operations suitable for automated workflows.

## UI Synchronization

Saved keywords remain accessible through the web interface. The UI route [`web/src/routes/_marketing/features/saved-keywords.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/saved-keywords.tsx) fetches data via the same `listSavedKeywords` MCP tool, guaranteeing consistency between API operations and the visual interface.

## Summary

- **Authentication**: Obtain an MCP token with `projectId` scope from [`web/content/docs/mcp.md`](https://github.com/every-app/open-seo/blob/main/web/content/docs/mcp.md) OAuth flows.
- **Endpoint**: POST to `https://app.openseo.so/mcp` with method `saveKeywords`.
- **Validation**: The service layer validates input using Zod schemas in [`src/server/services/keyword.service.ts`](https://github.com/every-app/open-seo/blob/main/src/server/services/keyword.service.ts).
- **Persistence**: Data writes to PostgreSQL via [`savedKeywordRepository.ts`](https://github.com/every-app/open-seo/blob/main/savedKeywordRepository.ts) using upsert logic in [`drizzle/0015_huge_celestials.sql`](https://github.com/every-app/open-seo/blob/main/drizzle/0015_huge_celestials.sql).
- **Idempotency**: Repeated saves update existing records rather than creating duplicates.
- **Integration**: UI components consume the same MCP tools for real-time synchronization.

## Frequently Asked Questions

### What authentication method does the OpenSEO MCP require?

The MCP requires a Bearer token obtained through OAuth or API key authentication. The token must include the `projectId` scope to authorize write operations to keyword data. Requests without this scope receive a `401 Unauthorized` response.

### Can I update existing keywords instead of creating duplicates?

Yes. The `saveKeywords` method is idempotent. The repository layer in [`src/server/repositories/savedKeywordRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/repositories/savedKeywordRepository.ts) executes an `INSERT … ON CONFLICT … DO UPDATE` query, allowing you to modify tags and notes on existing keywords without creating duplicate entries.

### How are tags stored in the database?

Tags are stored as JSON arrays in the `tags` column of the `saved_keyword` table. This structure, defined in [`drizzle/0015_huge_celestials.sql`](https://github.com/every-app/open-seo/blob/main/drizzle/0015_huge_celestials.sql), enables efficient filtering and clustering operations in downstream SEO workflows.

### Where can I find the UI implementation for viewing saved keywords?

The React route at [`web/src/routes/_marketing/features/saved-keywords.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/saved-keywords.tsx) implements the Saved Keyword Lists page. This component calls `listSavedKeywords` via the MCP to ensure the UI reflects the same data persisted through your API calls.