# Sound Recording MCP Server

A local MCP (Model Context Protocol) server for sound-recording workflows, providing integrated access to Neo4j, Snowflake, MySQL databases and AWS services (Lambda, Step Functions, S3).

## Features

- **Database Access**: Query Neo4j, Snowflake, and MySQL databases
- **AWS Integration**: Invoke Lambda functions, execute Step Functions, manage S3 buckets
- **Sound Recording Tools**: Query delivery history for sound recordings
- **Extensible**: Easy to add new tools and service integrations

---

## Setup

### 1. Create a virtual environment

```bash
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
```

### 2. Install dependencies

```bash
pip install -r requirements.txt
```

### 3. Stage credentials

Copy the example env file and fill in your values:

```bash
cp .env.example .env
```

`.env` is gitignored — never commit it. All credentials are loaded from this file at startup via `python-dotenv`. See the [Credentials reference](#credentials-reference) section below for where to find each value.

### 4. Create the MCP client config

```bash
cp .mcp.example.json .mcp.json
```

`.mcp.json` is also gitignored. Edit it if your Python binary is not `python` or if your client requires an absolute path.

### 5. Start the server

```bash
python src/mcp_server.py
```

This starts in `stdio` mode, which is what Claude Code expects. The server is ready as soon as it prints `Sound Recording MCP Server started`.

---

## Credentials Reference

All credentials live in `.env` (created from `.env.example`). The sections below explain each variable and where to find it.

### Neo4j

```env
NEO4J_URI=neo4j+ssc://your-cluster.example.com:7687
NEO4J_USERNAME=your_username
NEO4J_PASSWORD=your_password
```

| Variable | Where to find it |
|---|---|
| `NEO4J_URI` | Neo4j Aura console → "Connect" tab, or ask your DBA. Common schemes: `bolt://` (local), `neo4j+s://` (TLS), `neo4j+ssc://` (TLS + self-signed cert) |
| `NEO4J_USERNAME` | Your Neo4j user login |
| `NEO4J_PASSWORD` | Your Neo4j user password |

### Snowflake

```env
SNOWFLAKE_ACCOUNT=orchard
SNOWFLAKE_USER=QA_OWS_SOUND_RECORDINGS
SNOWFLAKE_DATABASE=FACTS
SNOWFLAKE_WAREHOUSE=QA_OWS_WAREHOUSE
SNOWFLAKE_SCHEMA=QA

# Pick ONE auth method:
SNOWFLAKE_KEY=<base64-encoded DER private key bytes>  # key-pair (preferred)
SNOWFLAKE_PASSWORD=your_password                      # password fallback
```

| Variable | Where to find it |
|---|---|
| `SNOWFLAKE_ACCOUNT` | Your Snowflake URL prefix: `https://<account>.snowflakecomputing.com` |
| `SNOWFLAKE_USER` | Snowflake username (ask your Snowflake admin) |
| `SNOWFLAKE_KEY` | See [Snowflake key-pair auth](#snowflake-key-pair-auth) below |
| `SNOWFLAKE_PASSWORD` | Snowflake password; used only when `SNOWFLAKE_KEY` is absent |
| `SNOWFLAKE_ROLE` | Optional role to activate for the session |
| `SNOWFLAKE_DATABASE` | Default database; can be overridden per-query |
| `SNOWFLAKE_WAREHOUSE` | Compute warehouse to use |
| `SNOWFLAKE_SCHEMA` | Default schema (falls back to `public`) |


### MySQL

```env
MYSQL_HOST=localhost
MYSQL_USER=root
MYSQL_PASSWORD=your_password
```

| Variable | Where to find it |
|---|---|
| `MYSQL_HOST` | Hostname or IP of the MySQL server |
| `MYSQL_USER` | MySQL username |
| `MYSQL_PASSWORD` | MySQL password |

The client always connects to port 3306. To use a non-default port, append it to `MYSQL_HOST` as `host:port` — the client splits on `:` if present.

### AWS

```env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_SESSION_TOKEN=your_session_token
```

This project uses **[awsume](https://awsu.me)** to obtain temporary credentials. Run it with the `--output-profile` flag to print credentials you can paste directly into `.env`:

```bash
awsume <your-profile-name> --show-commands
```

This prints `export AWS_ACCESS_KEY_ID=...` lines — copy the three values (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`) into your `.env` file.

Credentials from `awsume` are temporary and expire (typically after 1 hour). Refresh them by re-running the command above and updating the three values in `.env`.
---

## Verifying Connectivity (Health Checks)

Use `client_debug.py` to verify each service can connect **without running a separate server**. The script spawns the server as a subprocess and calls tools directly.

### Edit and run `client_debug.py`

Open `client_debug.py` and replace the `test_cases` list with the checks you want, then run:

```bash
python client_debug.py
```

#### Neo4j smoke test

```python
test_cases = [
    ("query_neo4j", {"cypher": "RETURN 1 AS ping"}, "Neo4j connectivity"),
]
```

Expected healthy output:
```json
{
  "status": "success",
  "results": [{"ping": 1}],
  "count": 1
}
```

#### Snowflake smoke test

```python
test_cases = [
    ("query_snowflake", {"sql": "SELECT CURRENT_DATE() AS today"}, "Snowflake connectivity"),
]
```

Expected healthy output:
```json
{
  "status": "success",
  "results": [{"TODAY": "2026-06-19"}],
  "count": 1
}
```

#### MySQL smoke test

```python
test_cases = [
    ("query_mysql", {"sql": "SELECT VERSION() AS version"}, "MySQL connectivity"),
]
```

Expected healthy output:
```json
{
  "status": "success",
  "results": [{"version": "8.0.36"}],
  "count": 1
}
```

#### AWS smoke tests

```python
test_cases = [
    ("list_s3_buckets", {}, "AWS / S3 connectivity"),
    ("list_lambda_functions", {}, "AWS / Lambda connectivity"),
    ("list_sfn_state_machines", {}, "AWS / Step Functions connectivity"),
]
```

Expected healthy output (S3 example):
```json
{
  "status": "success",
  "buckets": ["bucket-a", "bucket-b"],
  "count": 2
}
```

### Full connectivity sweep

To check all services in one run, use this `test_cases` block:

```python
test_cases = [
    ("query_neo4j",       {"cypher": "RETURN 1 AS ping"},              "Neo4j"),
    ("query_snowflake",   {"sql": "SELECT CURRENT_DATE() AS today"},   "Snowflake"),
    ("query_mysql",       {"sql": "SELECT VERSION() AS version"},       "MySQL"),
    ("list_s3_buckets",   {},                                           "AWS S3"),
    ("list_lambda_functions", {},                                       "AWS Lambda"),
]
```

### Health endpoint (SSE mode only)

When running in SSE mode the server exposes a lightweight HTTP health endpoint:

```bash
# Start the server in SSE mode
python src/mcp_server.py --transport sse --host 127.0.0.1 --port 55392

# In another terminal:
curl -s http://127.0.0.1:55392/health | python -m json.tool
```

Expected response:
```json
{
  "status": "ok",
  "transport": "sse",
  "sse_path": "/sse",
  "message_path": "/messages"
}
```

This endpoint confirms the HTTP listener is up but does **not** probe individual database connections — use `client_debug.py` for that.

---

## Running over SSE for `mcp-inspector`

```bash
python src/mcp_server.py --transport sse --host 127.0.0.1 --port 55392
```

Endpoints:

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/sse` | SSE stream for MCP clients |
| `POST` | `/messages` | Incoming MCP client messages |
| `GET` | `/health` | Lightweight liveness check |

Attach `mcp-inspector`:

```bash
npx -y @modelcontextprotocol/inspector \
  --transport sse \
  --server-url http://127.0.0.1:55392/sse
```

Transport env variables (all optional; CLI flags take precedence):

| Variable | Default | Description |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` | `stdio` or `sse` |
| `MCP_HOST` | `127.0.0.1` | Bind address (SSE only) |
| `MCP_PORT` | `55392` | Bind port (SSE only) |
| `MCP_SSE_PATH` | `/sse` | SSE GET endpoint |
| `MCP_MESSAGE_PATH` | `/messages` | Messages POST endpoint |

---

## Project Structure

```
src/
├── mcp_server.py                # Entry point; Tool definitions and call_tool dispatch
├── tools/                       # Thin wrappers: call client methods, return {status, ...}
│   ├── neo4j_tools.py
│   ├── snowflake_tools.py
│   ├── mysql_tools.py
│   ├── aws_tools.py
│   └── sound_recording_tools.py # Domain tools (get_delivery_history)
├── db/                          # Connection managers (lazy connect on first use)
│   ├── neo4j_client.py
│   ├── snowflake_client.py      # Key-pair (SNOWFLAKE_KEY as base64 DER) or password auth
│   └── mysql_client.py
└── aws/                         # boto3 wrappers
    ├── lambda_client.py
    ├── sfn_client.py
    └── s3_client.py
```

---

## Available Tools

### Sound Recording

| Tool | Description |
|---|---|
| `get_delivery_history` | Query sound recording delivery history from Snowflake |

### Database

| Tool | Description |
|---|---|
| `query_neo4j` | Execute a Cypher query |
| `find_neo4j_nodes` | Find nodes by label and optional property filters |
| `query_snowflake` | Execute a SQL query |
| `get_snowflake_table_info` | Get table metadata |
| `query_mysql` | Execute a SQL query |
| `get_mysql_table_schema` | Get table schema |

### AWS

| Tool | Description |
|---|---|
| `list_lambda_functions` | List Lambda functions |
| `get_lambda_info` | Get details about a Lambda function |
| `invoke_lambda` | Invoke a Lambda function (sync or async) |
| `list_sfn_state_machines` | List Step Functions state machines |
| `start_sfn_execution` | Start a Step Functions execution |
| `describe_sfn_execution` | Describe an execution |
| `get_sfn_execution_history` | Get execution event history |
| `list_s3_buckets` | List S3 buckets |
| `list_s3_objects` | List objects in a bucket |
| `get_s3_object` | Read an object from S3 |
| `get_s3_bucket_size` | Get total size statistics for a bucket |

---

## Adding New Tools

Three changes are required, all in `src/mcp_server.py` (plus implementing the function in `src/tools/`):

1. Add a `Tool(...)` entry in `list_tools()`
2. Add an `elif name == "..."` branch in `call_tool()`
3. Implement the function in the appropriate `src/tools/` module

All tool functions must return `{"status": "success", ...}` or `{"status": "error", "message": str(e)}`.

```python
# src/tools/my_module.py
def my_tool(param: str) -> dict:
    try:
        result = do_something(param)
        return {"status": "success", "result": result}
    except Exception as e:
        return {"status": "error", "message": str(e)}
```

```python
# src/mcp_server.py — list_tools()
Tool(
    name="my_tool",
    description="What my tool does",
    inputSchema={
        "type": "object",
        "properties": {
            "param": {"type": "string", "description": "Description of param"}
        },
        "required": ["param"]
    }
)

# src/mcp_server.py — call_tool()
elif name == "my_tool":
    result = my_module.my_tool(arguments["param"])
```

---

## Troubleshooting

### `status: error` from a tool

The tool itself caught an exception and returned it. The `message` field contains the original exception string — start there.

### "Connection refused" / "Cannot connect"

- Verify the host/port in `.env` are reachable from your machine
- For Neo4j: try `telnet <host> 7687`
- For MySQL: try `mysql -h $MYSQL_HOST -u $MYSQL_USER -p`
- For AWS: run `aws sts get-caller-identity` to confirm credentials are resolved

### "Authentication failed" (Neo4j / MySQL)

- Double-check the username and password in `.env`
- Special characters in passwords must not be shell-escaped inside `.env` — wrap the value in double quotes if it contains `#`, `=`, or spaces

### "JWT token is invalid" / Snowflake auth error

- Regenerate your Snowflake RSA key pair and update `SNOWFLAKE_KEY`
- Confirm the public key is registered on your Snowflake user: `DESC USER <your_user>;`
- If using a passphrase-protected key, switch to an unencrypted key or use `SNOWFLAKE_PASSWORD` instead

### "ExpiredTokenException" (AWS)

Temporary credentials have expired. Re-run `awsume` and paste the new values into `.env`:

```bash
awsume <your-profile-name> --show-commands
# copy the new AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN into .env
```

### Import errors on startup

- Confirm the virtual environment is activated (`which python` should point inside `venv/`)
- Run `pip install -r requirements.txt` again
- Run from the project root, not from inside `src/`
