npm package: @luutuankiet/looker-mcp-shim
Description: MCP server for AI-powered Looker development — tile inspection, compiled SQL, dev/prod mode, code execution escape hatch
Latest version: 0.4.2

# @luutuankiet/looker-mcp-shim

**One MCP server. Full Looker developer autonomy for AI agents.**

Inspect dashboards tile-by-tile. Create, modify, and delete tiles and filters. Run queries with compiled SQL. Edit LookML, push, validate. Execute any of Looker's 469 API endpoints via SDK method discovery. All through a single stdio server — no human-in-the-loop for mechanical steps.

```bash
npx -y @luutuankiet/looker-mcp-shim
```

**Install skill docs** for Claude Code agents (workflow guides, API patterns, recipes):

```bash
npx -y @luutuankiet/looker-mcp-shim install-skill          # current project
npx -y @luutuankiet/looker-mcp-shim install-skill --global  # all projects
```

---

## What This Does

An AI agent using this server can do everything a Looker developer does — including LookML dashboard lifecycle (import, iterate, export):

```mermaid
graph LR
    A[Inspect<br/>dashboards + tiles] --> B[Query<br/>data + compiled SQL]
    B --> C[Mutate<br/>create/update/delete<br/>tiles + filters]
    C --> D[Edit LookML<br/>push + validate]
    D --> E[Verify<br/>re-inspect + re-query]
    E --> A
    F[LookML Dashboard<br/>import → iterate → export] --> C
    A --> F

    style F fill:#9C27B0,color:#fff

    style A fill:#4285F4,color:#fff
    style B fill:#34A853,color:#fff
    style C fill:#FBBC04,color:#000
    style D fill:#EA4335,color:#fff
    style E fill:#4285F4,color:#fff
```

**57 tools** from one server: 17 custom shim tools + 41 dynamically bridged from Google's upstream Looker MCP.

---

## Architecture

```mermaid
graph TD
    subgraph "AI Agent"
        A["Claude Code / Cursor / etc"]
    end

    subgraph "looker-mcp-shim — single stdio server"
        direction TB
        D["core.ts<br/>Session + Safety Layer"]
        
        subgraph "Read Tools"
            E["inspect"]
            F["run_tile / run_query"]
        end
        
        subgraph "Mutation Tools"
            G["create_tile / update_tile / delete_tile"]
            H["create_filter / update_filter / delete_filter"]
        end
        
        subgraph "Dev Tools"
            I["switch_mode / reset_to_remote / validate"]
        end
        
        subgraph "Escape Hatch"
            J["retrieve_sdk_methods"]
            K["describe_sdk_method"]
            L["execute_sdk_code"]
        end
        
        U["upstream.ts<br/>MCP Client Bridge"]
    end

    subgraph "Google Looker MCP"
        V["@toolbox-sdk/server<br/>41 tools"]
    end

    subgraph "Looker"
        W["REST API 4.0"]
        X["swagger.json<br/>469 methods"]
    end

    A -->|stdio| D
    D --> E & F & G & H & I & J & K & L
    D --> U
    U -->|stdio| V
    V --> W
    D --> W
    J & K -.->|loads at startup| X

    style D fill:#4285F4,color:#fff
    style U fill:#34A853,color:#fff
    style V fill:#EA4335,color:#fff
```

---

## Quickstart

### 1. Configure

```bash
# .env (or pass as env vars in MCP config)
LOOKER_BASE_URL=https://your-instance.cloud.looker.com
LOOKER_CLIENT_ID=your_client_id
LOOKER_CLIENT_SECRET=your_client_secret
LOOKER_PROJECT_ID=your-lookml-project
# Branch auto-detected from Looker. Optional overrides:
# LOOKER_ALLOWED_BRANCHES=*                              # default: any branch
# LOOKER_RESET_BRANCHES=feat/my-branch,tmp/sandbox       # default: none
```

### 2. Register

**Claude Code / Cursor** — add to `.mcp.json`:

```json
{
  "mcpServers": {
    "looker": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@luutuankiet/looker-mcp-shim"],
      "env": {
        "LOOKER_BASE_URL": "https://your-instance.cloud.looker.com",
        "LOOKER_CLIENT_ID": "...",
        "LOOKER_CLIENT_SECRET": "...",
        "LOOKER_PROJECT_ID": "your-project",
        "LOOKER_RESET_BRANCHES": "feat/your-branch"
      }
    }
  }
}
```

### 3. Test without registration

```bash
npx @luutuankiet/mcp-proxy-shim passthru -- npx @luutuankiet/looker-mcp-shim

# From another terminal:
curl http://localhost:3456/tools              # list all 56 tools
curl -X POST http://localhost:3456/call/inspect -d '{"args":{"target":"151"}}'
```

Disable upstream bridge: `SKIP_UPSTREAM=1 npx @luutuankiet/looker-mcp-shim`

### 4. Install Skill Docs (recommended)

```bash
npx -y @luutuankiet/looker-mcp-shim install-skill
```

Installs workflow guides to `.claude/skills/looker-mcp-shim/`:

| File | What It Teaches |
|------|----------------|
| `SKILL.md` | Entry point + tool index + decision tree |
| `rules/workflow.md` | The complete dev loop |
| `rules/inspect.md` | Dashboard/tile inspection |
| `rules/query.md` | Running queries with filter auto-wiring |
| `rules/mutate.md` | Tile + filter CRUD, filter wiring via SDK |
| `rules/git-ops.md` | Dev mode, git sync, LookML validation |
| `rules/sdk-escape.md` | SDK method discovery + code execution |
| `rules/patterns.md` | Migration QA, dashboard cloning, bulk ops |

Only touches `looker-mcp-shim/` namespace. No interference with other skills.

---

## Branch Safety Model

Looker dev mode operates on git branches. The shim separates **switching** (safe) from **resetting** (destructive):

```mermaid
graph TD
    A[Agent wants to work<br/>on a branch] --> B{switch_mode}
    B -->|LOOKER_ALLOWED_BRANCHES=*| C[Switch to ANY branch]
    B -->|explicit list| D[Only listed branches]
    
    C --> E{Agent calls<br/>reset_to_remote?}
    D --> E
    
    E -->|Branch in<br/>LOOKER_RESET_BRANCHES| F[Reset allowed<br/>Wipes uncommitted changes]
    E -->|Branch NOT in<br/>LOOKER_RESET_BRANCHES| G[BLOCKED<br/>Other people's work protected]
    
    style F fill:#34A853,color:#fff
    style G fill:#EA4335,color:#fff
```

### Setup

```bash
# .env — branch is auto-detected from Looker, no config needed
LOOKER_ALLOWED_BRANCHES=*                            # switch to any branch (default)
LOOKER_RESET_BRANCHES=feat/dev_tools,tmp/sandbox     # ONLY these can be reset
```

### How It Works

| Action | Gate | Example |
|--------|------|---------|
| `switch_mode({mode: "dev", branch: "feat/alice"})` | `ALLOWED_BRANCHES` | ✅ With `*`, any branch works |
| `switch_mode({mode: "dev", branch: "main"})` | `ALLOWED_BRANCHES` | ✅ Safe — read-only inspection per session |
| `reset_to_remote({})` on `feat/dev_tools` | `RESET_BRANCHES` | ✅ In the list — reset allowed |
| `reset_to_remote({})` on `feat/alice` | `RESET_BRANCHES` | ❌ BLOCKED — not in list |
| `reset_to_remote({})` on `main` | `RESET_BRANCHES` | ❌ BLOCKED — not in list |

### Typical Branch Workflow

```mermaid
sequenceDiagram
    participant Agent
    participant Shim
    participant Looker

    Note over Agent: Start on default dev branch
    Agent->>Shim: switch_mode({mode: "dev",<br/>branch: "feat/dev_tools"})
    Shim-->>Agent: {mode: "dev", branch: "feat/dev_tools"}

    Note over Agent: Inspect another team member's branch
    Agent->>Shim: switch_mode({mode: "dev",<br/>branch: "feat/alice-dashboard"})
    Shim-->>Agent: {mode: "dev", branch: "feat/alice-dashboard"}
    Agent->>Shim: inspect({target: "152"})
    Shim-->>Agent: Dashboard state on Alice's branch

    Note over Agent: Try to reset — BLOCKED
    Agent->>Shim: reset_to_remote({})
    Shim-->>Agent: ERROR: Branch "feat/alice-dashboard"<br/>not in LOOKER_RESET_BRANCHES

    Note over Agent: Switch to safe branch, edit, reset
    Agent->>Shim: switch_mode({mode: "dev",<br/>branch: "feat/dev_tools"})
    Note over Agent: Edit LookML, git push
    Agent->>Shim: reset_to_remote({})
    Shim-->>Agent: {success: true}
    Agent->>Shim: validate({})
    Shim-->>Agent: {status: "ok"}
```

**Why this matters:** `reset_to_remote` wipes all uncommitted changes on the current branch for the API user. Without the safety gate, an agent switching to a colleague's branch and resetting could destroy work-in-progress.

---

## Agent Workflows

### 1. Dashboard Inspection

The agent explores a dashboard in two levels — overview first, then drill into specific tiles.

```mermaid
sequenceDiagram
    participant Agent
    participant Shim
    participant Looker

    Agent->>Shim: inspect({target: "152"})
    Shim->>Looker: GET /dashboard_elements + /dashboard_filters
    Shim-->>Agent: 12 tiles, 14 filters<br/>(~50 tokens/tile)

    Note over Agent: Agent picks tile to investigate

    Agent->>Shim: inspect({target: "tile:1486"})
    Shim->>Looker: GET /dashboard_elements/1486
    Shim-->>Agent: fields, filters, sorts, vis_config,<br/>filter wiring, query_id (~200 tokens)

    Agent->>Shim: run_tile({element_id: "1486", format: "sql"})
    Shim->>Looker: GET /queries/{id}/run/sql
    Shim-->>Agent: Compiled BigQuery SQL (33K chars)

    Agent->>Shim: run_tile({element_id: "1486", format: "json", limit: 5})
    Shim->>Looker: GET /queries/{id}/run/json
    Shim-->>Agent: 5 data rows
```

**URL-smart input** — all of these work:

| Input | Parses To |
|-------|----------|
| `"152"` | Dashboard 152 |
| `"tile:1486"` | Tile detail |
| `"https://host/dashboards/152"` | Dashboard 152 |
| `"https://host/explore/common/transactions?fields=..."` | Explore |

---

### 2. Dashboard Mutation — Build & Modify Tiles

The agent creates tiles, modifies them, and verifies — full CRUD without touching the Looker UI.

```mermaid
sequenceDiagram
    participant Agent
    participant Shim
    participant Looker

    Note over Agent: Create a revenue tile
    Agent->>Shim: create_tile({dashboard_id: "152",<br/>title: "Revenue by Region",<br/>query: {model: "common",<br/>view: "transactions",<br/>fields: ["transactions.region",<br/>"transactions.total_revenue"],<br/>vis_config: {type: "looker_bar"}}})
    Note over Shim: Two-step: create_query() -> query_id<br/>then create_dashboard_element()
    Shim->>Looker: POST /queries + POST /dashboard_elements
    Shim-->>Agent: {element_id: "1503"}

    Note over Agent: Change chart type to pie
    Agent->>Shim: update_tile({element_id: "1503",<br/>query: {vis_config: {type: "looker_pie"}}})
    Note over Shim: Reads existing query, merges changes,<br/>creates new query, updates element
    Shim->>Looker: GET element -> POST /queries -> PATCH element
    Shim-->>Agent: {fields preserved, vis updated}

    Note over Agent: Add a filter
    Agent->>Shim: create_filter({dashboard_id: "152",<br/>name: "region_filter",<br/>title: "Region",<br/>type: "field_filter",<br/>dimension: "transactions.region",<br/>model: "common",<br/>explore: "transactions"})
    Shim->>Looker: POST /dashboard_filters
    Shim-->>Agent: {filter_id: "1141"}

    Note over Agent: Verify everything
    Agent->>Shim: inspect({target: "152"})
    Shim-->>Agent: Tile 1503 + Filter 1141 confirmed
```

**Key insight:** Looker's API rejects inline `query` objects on element create/update. The shim handles the two-step dance automatically — `create_query()` first, then reference via `query_id`. Agents don't need to know this.

**Partial updates:** When updating a tile's `vis_config`, the shim reads the existing query, merges your changes, and preserves everything else (fields, filters, sorts). You only send what changed.

---

### 3. LookML Edit → Validate → Verify Loop

The complete developer loop: edit LookML, sync Looker, validate, check results.

```mermaid
sequenceDiagram
    participant Agent
    participant Git as Local Git
    participant Shim
    participant Looker
    participant BQ as BigQuery

    Note over Agent: Agent edits LookML locally
    Agent->>Git: Edit .lkml files
    Agent->>Git: git push origin feat/branch

    Note over Agent: Sync Looker to latest code
    Agent->>Shim: reset_to_remote({})
    Shim->>Looker: POST /projects/{id}/reset_to_remote
    Shim-->>Agent: {success: true}

    Note over Agent: Validate LookML syntax
    Agent->>Shim: validate({})
    Shim->>Looker: POST /projects/{id}/lookml_validation
    Shim-->>Agent: {status: "ok", errors: []}

    Note over Agent: Verify data still correct
    Agent->>Shim: run_tile({element_id: "1486",<br/>format: "json", limit: 5})
    Shim->>Looker: GET /queries/{id}/run/json
    Looker->>BQ: Execute compiled SQL
    BQ-->>Looker: Result rows
    Shim-->>Agent: [{region: "SG", revenue: 45000}, ...]

    Note over Agent: Compare with expected values
    Agent->>Shim: run_tile({element_id: "1486",<br/>format: "json", limit: 5,<br/>force_production: true})
    Shim-->>Agent: Production data for parity check
```

**`force_production`** — Run a query against production LookML from dev mode. Perfect for comparing dev changes against prod baseline without switching modes.

**Long-running queries** — Complex explores (e.g., BigQuery with 33K char compiled SQL) get 120s timeout by default. If that expires, the shim automatically falls back to Looker's async query task mechanism (same as the Looker UI), polls for completion, and returns results.

---

### 4. SDK Method Discovery — The Escape Hatch

For anything not covered by the 15 dedicated tools, the agent discovers and executes any of Looker's 469 API methods.

```mermaid
sequenceDiagram
    participant Agent
    participant Shim
    participant Swagger as swagger.json

    Note over Agent: "I need to find scheduled plans"<br/>Step 1: Search
    Agent->>Shim: retrieve_sdk_methods({query: "schedule plan"})
    Shim->>Shim: Search catalog (loaded from swagger.json)
    Shim-->>Agent: 15 matches:<br/>scheduled_plans_for_dashboard,<br/>create_scheduled_plan, ...

    Note over Agent: Step 2: Get details
    Agent->>Shim: describe_sdk_method({<br/>method: "scheduled_plans_for_dashboard"})
    Shim-->>Agent: Parameters: dashboard_id (required),<br/>user_id, all_users, fields<br/>Code example: sdk.ok(sdk.scheduled_plans_for_dashboard({<br/>  dashboard_id: "152" }))

    Note over Agent: Step 3: Execute
    Agent->>Shim: execute_sdk_code({code: "<br/>const plans = await sdk.ok(<br/>  sdk.scheduled_plans_for_dashboard({<br/>    dashboard_id: '152', all_users: true<br/>  })<br/>)<br/>return plans.map(p => ({<br/>  name: p.name, cron: p.crontab<br/>}))"})
    Shim-->>Agent: [{name: "Weekly Revenue",<br/>cron: "0 9 * * 1"}, ...]
```

The catalog is loaded from the Looker instance's own `swagger.json` at startup — always accurate for your specific Looker version.

---

### 5. Migration QA — Tableau to Looker

The use case that drove this tool: an agent receives a Tableau screenshot and verifies data parity in Looker, tile by tile.

```mermaid
sequenceDiagram
    participant Human
    participant Agent
    participant Shim
    participant Looker

    Human->>Agent: "Verify dashboard 152 matches<br/>this Tableau screenshot"

    Agent->>Shim: switch_mode({mode: "dev",<br/>branch: "feat/migration"})
    Shim-->>Agent: {mode: "dev", branch: "feat/migration"}

    Agent->>Shim: inspect({target: "152"})
    Shim-->>Agent: 12 tiles, 14 filters

    loop For each tile
        Agent->>Shim: inspect({target: "tile:{id}"})
        Shim-->>Agent: fields, model, explore, filters

        Agent->>Shim: run_tile({element_id: "{id}",<br/>format: "json", limit: 20})
        Shim-->>Agent: Data rows

        Note over Agent: Compare data vs Tableau screenshot

        alt Data mismatch found
            Agent->>Shim: run_tile({element_id: "{id}",<br/>format: "sql"})
            Shim-->>Agent: Compiled SQL
            Note over Agent: Agent identifies join/filter issue,<br/>edits LookML, pushes to git
            Agent->>Shim: reset_to_remote({})
            Agent->>Shim: validate({})
            Agent->>Shim: run_tile({element_id: "{id}",<br/>format: "json"})
            Note over Agent: Data matches now
        end
    end

    Agent->>Human: "All 12 tiles verified.<br/>3 had issues, fixed in LookML."
```

---

### 6. LookML Dashboard Lifecycle — Import, Iterate, Export

LookML dashboards are code-defined — tiles cannot be mutated via API. The shim provides a fast iteration path: import as UDD, iterate with mutation tools (no git commits), then export back to LookML.

```mermaid
sequenceDiagram
    participant Agent
    participant Shim
    participant Looker
    participant Git

    Note over Agent: Inspect the LookML dashboard
    Agent->>Shim: inspect({target: "model::dashboard_name"})
    Shim->>Looker: GET /dashboards/model::dashboard_name
    Shim-->>Agent: 4 tiles, 14 filters, type: lookml_dashboard<br/>hint: use import_lookml_dashboard

    Note over Agent: Import as editable UDD
    Agent->>Shim: import_lookml_dashboard({<br/>lookml_dashboard_id: "model::dashboard_name",<br/>folder_id: "85"})
    Shim->>Looker: validate_project + dashboard() + import_lookml_dashboard()
    Shim-->>Agent: {id: "173", verification: {tiles_match: true}}

    Note over Agent: Fast iteration loop (no git!)
    loop Iterate on UDD
        Agent->>Shim: update_tile / create_tile / update_filter
        Agent->>Shim: run_tile({dashboard_id: "173", tile: "#1"})
        Agent->>Shim: inspect({target: "173"})
    end

    Note over Agent: Export final state as LookML
    Agent->>Shim: export_dashboard_lookml({dashboard_id: "173"})
    Shim->>Looker: GET /dashboards/173/lookml
    Shim-->>Agent: {lookml: "---\n- dashboard: ..."}

    Note over Agent: Commit to code
    Agent->>Git: Write .dashboard.lookml + git push
    Agent->>Shim: reset_to_remote({})
    Agent->>Shim: validate({})
    Agent->>Shim: inspect({target: "model::dashboard_name"})
    Note over Agent: Compiled result matches intent ✔
```

**Two loops, one bridge:**
- **Fast loop (UDD):** mutation tools, instant, no git
- **Slow loop (LookML):** git push + reset + validate
- **import** enters the fast loop, **export** exits it

---

## Tool Reference

### Inspection

| Tool | What It Does | Key Args |
|------|-------------|----------|
| `inspect` | Dashboard overview or tile detail | `target`: URL, ID, or `tile:NNN` |


### LookML Dashboard Lifecycle

| Tool | What It Does | Key Args |
|------|-------------|----------|
| `inspect` | Inspect LookML dashboard (tiles, filters) | `target`: `"model::dashboard_name"` |
| `import_lookml_dashboard` | Clone LookML dashboard as editable UDD | `lookml_dashboard_id`, `folder_id` |
| `export_dashboard_lookml` | Export any dashboard as LookML YAML | `dashboard_id` |

### Query Execution

| Tool | What It Does | Key Args |
|------|-------------|----------|
| `run_tile` | Execute a tile's query | `element_id`, `format` (json/sql/csv), `limit`, `force_production`, `timeout` |
| `run_query` | Ad-hoc explore query | `model`, `explore`, `fields`, `filters`, `sorts`, `limit`, `force_production`, `timeout` |

Both tools handle long-running BigQuery queries automatically — 120s timeout with async query task fallback.

### Dashboard Mutation

| Tool | What It Does | Key Args |
|------|-------------|----------|
| `create_tile` | Add tile to dashboard | `dashboard_id`, `title`, `query` (inline) or `query_id` |
| `update_tile` | Modify tile (partial) | `element_id`, `title`, `query` (partial merge) |
| `delete_tile` | Remove tile | `element_id` |
| `create_filter` | Add dashboard filter | `dashboard_id`, `name`, `title`, `type`, `dimension`, `model`, `explore` |
| `update_filter` | Modify filter | `filter_id`, `title`, `default_value`, ... |
| `delete_filter` | Remove filter | `filter_id` |

**Partial query updates:** `update_tile` merges your changes with the existing query. Send only what changed — fields, vis_config, sorts, and filters are preserved.

```json
{"element_id": "1486", "query": {"vis_config": {"type": "looker_pie"}}}
```

The shim reads the current query, merges your `vis_config` change, creates a new query, and updates the element. Fields, filters, sorts — all preserved.

### Dev Operations

| Tool | What It Does |
|------|-------------|
| `switch_mode` | Toggle dev/prod mode with branch selection |
| `reset_to_remote` | Sync Looker project to git HEAD |
| `validate` | LookML syntax validation with file:line errors |

### SDK Escape Hatch

| Tool | What It Does |
|------|-------------|
| `retrieve_sdk_methods` | Search 469 SDK methods by keyword/tag |
| `describe_sdk_method` | Get full params + code example for a method |
| `execute_sdk_code` | Run arbitrary SDK code with pre-authenticated session |

**Workflow:** Search -> Describe -> Execute. Never guess method signatures.

```
retrieve_sdk_methods({query: "render png"})        # find the method
describe_sdk_method({method: "create_dashboard_element_render_task"})  # get params
execute_sdk_code({code: "..."})                     # run it
```

### Upstream Looker MCP (41 tools, auto-bridged)

All tools from `@toolbox-sdk/server --prebuilt=looker,looker-dev` are automatically available:

`get_project_files`, `update_project_file`, `create_project_file`, `delete_project_file`, `get_explores`, `get_dimensions`, `get_measures`, `get_filters`, `get_models`, `get_connections`, `run_dashboard`, `run_look`, `query_sql`, `query`, `make_dashboard`, `make_look`, `dev_mode`, `validate_project`, `get_dashboards`, `get_looks`, and more.

These are prefixed with `[upstream]` in descriptions. Our shim tools take priority on name collisions.

---

## Safety Layer

### Blocked SDK Methods (hardcoded, non-configurable)

| Category | Methods |
|----------|--------|
| **Production deployment** | `deploy_ref_to_production`, `deploy_to_production` |
| **User impersonation** | `login_user` |
| **Destructive admin** | `delete_group`, `create_group`, `update_group`, `delete_user_attribute`, `delete_role`, `delete_folder`, `delete_dashboard`, `delete_look`, `update_user` |
| **Schedule manipulation** | `delete_scheduled_plan`, `update_scheduled_plan`, `create_scheduled_plan` |

### Configurable Guardrails

| Setting | Purpose |
|---------|--------|
| `LOOKER_ALLOWED_BRANCHES` | Branch allowlist for `switch_mode`. Set `*` for any branch |
| `LOOKER_RESET_BRANCHES` | Branches where `reset_to_remote` is allowed (defaults to none) |
| `LOOKER_SANDBOX_FOLDER_ID` | Restrict dashboard saves to folder |

---

## Configuration

| Variable | Required | Default | Description |
|----------|----------|---------|------------|
| `LOOKER_BASE_URL` | **Yes** | — | Looker instance URL |
| `LOOKER_CLIENT_ID` | **Yes** | — | API client ID |
| `LOOKER_CLIENT_SECRET` | **Yes** | — | API client secret |
| `LOOKER_PROJECT_ID` | No | `''` | LookML project ID |
| `LOOKER_ALLOWED_BRANCHES` | No | `*` | Branch allowlist for switch_mode. `*` = any branch |
| `LOOKER_RESET_BRANCHES` | No | `''` (none) | Branches where `reset_to_remote` is allowed. Must be explicit |
| `LOOKER_SANDBOX_FOLDER_ID` | No | — | Default folder for import_lookml_dashboard |
| `SKIP_UPSTREAM` | No | — | Set to `1` to disable upstream bridge |

---

## How It Compares

| Capability | Google Looker MCP | This Shim |
|-----------|------------------|----------|
| LookML file CRUD | \u2705 | \u2705 (via upstream bridge) |
| Run dashboard (full) | \u2705 | \u2705 (via upstream bridge) |
| Per-tile inspection | \u274c | \u2705 fields, filters, vis_config, filter wiring |
| Per-tile compiled SQL | \u274c | \u2705 run_tile format=sql |
| Create/update/delete tiles | \u274c | \u2705 with partial merge |
| Create/update/delete filters | \u274c | \u2705 |
| Reset to remote | \u274c | \u2705 |
| Dev/prod mode + branch | \u274c atomic | \u2705 atomic switch |
| SDK method discovery | \u274c | \u2705 retrieve + describe from swagger |
| Arbitrary SDK execution | \u274c | \u2705 with safety proxy |
| Long query handling | \u274c | \u2705 120s timeout + async fallback |
| **Total tools** | **41** | **57** (17 shim + 41 bridged, 1 shared name) |

You only register one server. It bridges the upstream automatically.

---

## Tech Stack

| Component | Package | Purpose |
|-----------|---------|--------|
| Runtime | Node.js >=20 (ESM) | Modern JavaScript |
| Language | TypeScript ^5.7 | Type safety |
| Looker SDK | `@looker/sdk-node` | Auth, session, API |
| MCP Server | `@modelcontextprotocol/sdk` | Server + Client (for bridge) |
| Upstream | `@toolbox-sdk/server` | Google's Looker MCP (bridged) |
| Testing | `@luutuankiet/mcp-proxy-shim` | Passthru REST testing |

## License

MIT