# Advanced Brief: Standalone MCP Server (Power Users)

> **For:** Technical strategists or developers who want to run their own MCP server
> **Purpose:** Build an MCP server that reads a Google Sheet Pattern Graph and exposes it via the Model Context Protocol
> **Date:** 2026-03-16

---

## What You're Building

A lightweight MCP server that:
1. Reads your Google Sheet (following the Fodda Pattern Standard)
2. Exposes it as MCP tools that Claude, Gemini, or Cursor can query
3. Can run locally or be deployed to Cloudflare Workers / Vercel / Cloud Run

This is for **Sophisticated tier** users who want full control over their MCP — or who want to test their graph before registering it on Fodda.

---

## Tech Stack

- **Runtime:** Node.js
- **MCP SDK:** `@modelcontextprotocol/sdk`
- **Google Sheets:** `googleapis` (v4 API)
- **Transport:** Streamable HTTP (for remote) or stdio (for local Claude Desktop)

---

## Setup

```bash
mkdir my-pattern-mcp && cd my-pattern-mcp
npm init -y
npm install @modelcontextprotocol/sdk googleapis express zod
npm install -D typescript @types/node @types/express
```

**Environment variables:**
```
GOOGLE_SHEETS_ID=your-sheet-id
GOOGLE_SERVICE_ACCOUNT_KEY=./credentials.json
PORT=8080
```

---

## MCP Tools to Implement

Your server exposes 5 tools that mirror the Fodda MCP tools:

### 1. `list_patterns`
- **Description:** "List all patterns in this graph with their descriptions and signal counts"
- **Input:** none (or optional sector filter)
- **Output:** Array of patterns from the Patterns tab, each with:
  - `patternId`, `patternName`, `description`, `sector`, `industry`, `confidence`
  - `signalCount` (computed: count of signals where `pattern_name` matches)
- **Maps to Fodda's:** `list_graphs` (but for a single graph's contents)

### 2. `search_patterns`
- **Description:** "Search patterns and signals by keyword"
- **Input:** `{ query: string, limit?: number }`
- **Output:** Matching patterns + signals, ranked by keyword relevance
- **Implementation:** Simple keyword matching on pattern names/descriptions + signal titles/summaries. Check if query terms appear in text. Rank by match count.
- **Maps to Fodda's:** `search_graph`

### 3. `get_signals`
- **Description:** "Get supporting signals (articles/evidence) for a specific pattern"
- **Input:** `{ patternName: string }` or `{ patternId: number }`
- **Output:** Array of signals where `pattern_name` matches, including title, sourceUrl, summary, brands, etc.
- **Maps to Fodda's:** `get_evidence`

### 4. `get_entities`
- **Description:** "List entities (brands, technologies, audiences) and their pattern connections"
- **Input:** `{ type?: string }` (optional filter: Brand / Technology / Audience / Location)
- **Output:** Array of entities from the Entities tab
- **Maps to Fodda's:** `get_label_values`

### 5. `get_graph_meta`
- **Description:** "Get metadata about this Pattern Graph — author, sectors, coverage dates, counts"
- **Input:** none
- **Output:** Key-value pairs from the Graph Meta tab + computed stats (signal count, pattern count, entity count, date range)

---

## Data Layer

### Reading the Sheet

```typescript
import { google } from 'googleapis';

const auth = new google.auth.GoogleAuth({
  keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY,
  scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
});
const sheets = google.sheets({ version: 'v4', auth });

async function readTab(sheetId: string, tabName: string): Promise<any[][]> {
  const res = await sheets.spreadsheets.values.get({
    spreadsheetId: sheetId,
    range: `${tabName}!A1:Z`,
  });
  return res.data.values || [];
}

function rowsToObjects(rows: any[][]): Record<string, string>[] {
  const [headers, ...data] = rows;
  return data.map(row =>
    Object.fromEntries(headers.map((h: string, i: number) => [h, row[i] || '']))
  );
}
```

### Caching

Cache the Sheet data in memory. Re-read every 5 minutes (or on demand):

```typescript
let cache: { signals: any[]; patterns: any[]; entities: any[]; meta: any } | null = null;
let lastRead = 0;

async function getData() {
  if (cache && Date.now() - lastRead < 5 * 60 * 1000) return cache;
  
  const sheetId = process.env.GOOGLE_SHEETS_ID!;
  cache = {
    signals: rowsToObjects(await readTab(sheetId, 'Signals')),
    patterns: rowsToObjects(await readTab(sheetId, 'Patterns')),
    entities: rowsToObjects(await readTab(sheetId, 'Entities')),
    meta: rowsToObjects(await readTab(sheetId, 'Graph Meta')),
  };
  lastRead = Date.now();
  return cache;
}
```

---

## MCP Server Setup

```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

function createServer(): McpServer {
  const server = new McpServer({
    name: 'my-pattern-graph',
    version: '1.0.0',
  });

  server.tool('search_patterns', 'Search patterns and signals by keyword', {
    query: z.string(),
    limit: z.number().optional(),
  }, async ({ query, limit }) => {
    const data = await getData();
    const q = query.toLowerCase();
    const matchingPatterns = data.patterns
      .filter(p => p.pattern_name?.toLowerCase().includes(q) || p.description?.toLowerCase().includes(q))
      .slice(0, limit || 10);
    return { content: [{ type: 'text', text: JSON.stringify(matchingPatterns, null, 2) }] };
  });

  // ... register other tools similarly

  return server;
}

// Transport handling (same pattern as Fodda MCP)
const transports = new Map();

app.all('/mcp', async (req, res) => {
  // ... streamable HTTP transport handler
  // See Fodda MCP src/index.ts for the full pattern
});

app.listen(parseInt(process.env.PORT || '8080'));
```

---

## Running Locally with Claude Desktop

For local use, you can run the server with stdio transport instead of HTTP:

```typescript
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
```

Add to Claude Desktop's `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "my-pattern-graph": {
      "command": "npx",
      "args": ["tsx", "/path/to/your/server.ts"],
      "env": {
        "GOOGLE_SHEETS_ID": "your-sheet-id",
        "GOOGLE_SERVICE_ACCOUNT_KEY": "/path/to/credentials.json"
      }
    }
  }
}
```

---

## Registering on Fodda

Once your MCP server is deployed (e.g., to Cloud Run or Vercel), register it on Fodda:

1. Go to app.fodda.ai → My Graphs → Register New Graph
2. Choose "I run my own MCP server"
3. Enter your MCP endpoint URL
4. Fodda will ping your server to verify it responds
5. Your graph appears in `list_graphs` for all Fodda MCP users

---

## Deployment Options

| Platform | Difficulty | Cost | Notes |
|---|---|---|---|
| Local (stdio) | Easy | Free | Claude Desktop only, not accessible remotely |
| Cloud Run | Medium | ~$0/month (free tier) | Deploy via `gcloud run deploy` with Dockerfile |
| Vercel | Medium | Free tier | Serverless, may have cold start issues with Sheets API |
| Cloudflare Workers | Hard | Free tier | Need to adapt for Workers runtime |

---

## Testing

1. Start the server locally
2. Connect Claude Desktop via stdio config
3. Ask Claude: "What patterns are in my graph?" → should call `list_patterns`
4. Ask Claude: "Search for signals about Nike" → should call `search_patterns`
5. Ask Claude: "Show me the evidence for [pattern name]" → should call `get_signals`
6. Verify the data matches what's in your Google Sheet
