Skip to main content
Glama
mzoffoli85

MPC-PoC-2-ResourceProvider-GoogleSheets

by mzoffoli85
README.md
# MPC-PoC-2-ResourceProvider-GoogleSheets

MCP server que expone Google Sheets como **Resources** (lectura), no como Tools.
Segunda PoC de la serie "Aprender MCP en profundidad".

## Qué expone

- **Resource estático** — `sheet://main/data`: contenido completo de la hoja principal (`MAIN_SHEET_NAME`, rango `MAIN_SHEET_RANGE`).
- **Resource template** — `sheet://{sheet_name}/{cell_range}`: lee cualquier hoja/rango dinámicamente. Ej: `sheet://Ventas/A1:D50`. Si el nombre de la hoja tiene espacios, codificarlo en la URI (ej. `sheet://Hoja%201/A1:C10`).

Ambos devuelven el contenido como CSV (`mimeType: text/csv`).

## Setup

### 1. Google Cloud

1. Crear (o reusar) un proyecto en [Google Cloud Console](https://console.cloud.google.com/).
2. Habilitar la **Google Sheets API** para ese proyecto (APIs & Services → Enable APIs → "Google Sheets API").
3. Crear un **Service Account** (IAM & Admin → Service Accounts → Create Service Account).
4. Generar una **key JSON** para ese service account (pestaña "Keys" → Add Key → JSON) y descargarla.
5. **Compartir la Google Sheet** con el email del service account (algo como `nombre@proyecto.iam.gserviceaccount.com`), con permiso de **lector**. Este paso se olvida siempre y produce un error 403.

### 2. Proyecto local

```bash
uv sync
cp .env.example .env
```

Completar `.env`:

```
SPREADSHEET_ID=<el ID de la spreadsheet, va en la URL entre /d/ y /edit>
GOOGLE_APPLICATION_CREDENTIALS=credentials.json
MAIN_SHEET_NAME=Sheet1
MAIN_SHEET_RANGE=A1:Z1000
```

Guardar la key JSON del service account como `credentials.json` en la raíz del proyecto (o ajustar la ruta en `GOOGLE_APPLICATION_CREDENTIALS`). **Nunca commitear este archivo** — ya está en `.gitignore`.

### 3. Correr el server

```bash
uv run python server.py
```

El server habla MCP sobre **stdio**. Para probarlo interactivamente:

```bash
uv run mcp dev server.py
```

Para usarlo desde Claude Desktop, agregar en `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "sheets-resources": {
      "command": "uv",
      "args": ["--directory", "/ruta/absoluta/a/este/proyecto", "run", "python", "server.py"]
    }
  }
}
```

Reiniciar Claude Desktop y verificar que el server aparece con los dos resources listados.

## Estructura

```
server.py           # MCP server: registra los resources
sheets_client.py     # wrapper de la Google Sheets API (auth + read_range)
.env.example         # variables de entorno de referencia
.gitignore           # excluye credentials*.json y .env
```

## Variables de entorno

| Variable | Requerida | Default | Descripción |
|---|---|---|---|
| `SPREADSHEET_ID` | Sí | — | ID de la spreadsheet a exponer |
| `GOOGLE_APPLICATION_CREDENTIALS` | No | `credentials.json` | Ruta a la key JSON del service account |
| `MAIN_SHEET_NAME` | No | `Sheet1` | Hoja que expone el resource estático |
| `MAIN_SHEET_RANGE` | No | `A1:Z1000` | Rango que expone el resource estático |

## Qué aprendí

- **Resources son application-controlled, no model-controlled**: a diferencia de un Tool, el LLM no "decide ejecutar" un resource con side effects — el cliente decide qué exponer y cuándo leerlo como contexto. El server solo declara qué existe y cómo leerlo.
- **La URI es el contrato**: un resource se identifica por su URI (`sheet://main/data`), no por un nombre de función. Eso permite que resources estáticos y templated convivan bajo el mismo esquema (`sheet://{sheet_name}/{cell_range}`), sin necesidad de "argumentos" en el sentido de Tools.
- **Templates son URIs con match, no RPC con parámetros validados por schema**: el SDK resuelve `{sheet_name}` y `{cell_range}` vía regex sobre la URI (cada segmento sin `/`), y recién ahí invoca la función — es más parecido a un router HTTP que a la firma de un Tool.
- **Cuándo elegir Resource vs Tool**: si la acción es "dame datos para que los uses como contexto" → Resource. Si es "hacé algo (leer con efectos, escribir, calcular, invocar una API con params arbitrarios que decide el modelo)" → Tool. Google Sheets de solo lectura calza perfecto como Resource porque no hay acción que "invocar": es data que el LLM lee, igual que leería un archivo.


![alt text](image.png)