npm package: @offlinemediainc/partners-mcp
Description: MCP server for the Partners submission API
Latest version: 2.4.0

## Partners MCP Server

Thin MCP server that exposes the existing `/api/llm/**` HTTP endpoints as Claude/LLM tools. Everything lives under `packages/partners-mcp/` so it can be removed or published without touching the Next.js app.

### Prerequisites

- Node 18+ (Claude Desktop uses stdio transport)
- Env vars:
  - `API_URL` – base URL for the Partners app (fallbacks to `NEXT_PUBLIC_SITE_URL`, then `http://localhost:3000`).
  - `LLM_MCP_API_KEY` (optional) – API key generated via `/admin/settings/api-keys`, sent as `x-api-key`.
  - `LLM_MCP_API_TOKEN` (optional) – legacy bearer token if you don't want per-client keys.

### Run locally

```bash
pnpm --filter @offlinemediainc/partners-mcp dev
```

Claude Desktop `claude_desktop_config.json` example:

```json
{
  "mcpServers": {
    "partners-submissions": {
      "command": "npx",
      "args": ["-y", "@offlinemediainc/partners-mcp"],
      "env": {
        "API_URL": "https://partners.yourdomain.com",
        "LLM_MCP_API_KEY": "partners_abc123"
      }
    }
  }
}
```

Tools registered:

| Tool | Description |
| --- | --- |
| `list_submissions` | Search existing submissions (prevent duplicates). |
| `get_submission` | Fetch a submission snapshot. |
| `duplicate_submission` | Clone an existing submission (clears event link, resets stage to draft). |
| `create_submission` | Create a new submission via the LLM API. |
| `link_submission_contact` | Link or create a HubSpot contact + partner user. |
| `link_submission_event` | Associate a backend event ID. |
| `move_submission_stage` | Force move a submission to another workflow stage. |
| `list_submission_stages` | Return workflow stage metadata. |

## Workflow stages

| Stage ID | Label | Status |
| --- | --- | --- |
| `submission_received` | Submission Received | Active |
| `internal_assessment` | Internal Assessment | Active |
| `concept_development_admin` | Concept Prep | Active |
| `concept_development` | Concept Development | Active |
| `draft_event_admin` | Draft Prep | Active |
| `draft_event` | Draft Event | Active |
| `waiting_launch` | Launch Prep | Active |
| `event_live` | Event Live | Active |
| `event_recap_admin` | Recap Prep | Active |
| `event_recap` | Event Recap (Terminal) | Completed |

Stages mirror `/api/llm/metadata/stages`. `activeOnly: true` means “exclude submissions currently at `event_recap`.” Use the `stage` filter or the metadata endpoint when you need to reason about specific transitions.

## Tool reference

Each MCP tool proxies a locked-down HTTP route under `/api/llm/**`. All responses wrap data in `{ success, data }` envelopes when called directly; the MCP server unwraps that for the client.

### `list_submissions`

Search existing submissions before creating a new one; call this whenever you need to avoid duplicates.

**Inputs**

- `search` – free text for restaurant/ contact name / contact email.
- `contactEmail` – exact case-insensitive match (takes precedence over `search`).
- `limit` – integer between 1 and 50 (defaults to 25).
- `activeOnly` – when `true`, filters out submissions whose workflow stage is `event_recap`.
- `stage` – workflow stage ID (`submission_received` … `event_recap`). Overrides `activeOnly` if both are supplied.

**Returns**

```jsonc
{
  "submissions": [
    {
      "id": "uuid",
      "restaurantName": "Lady Justice",
      "contactName": "Avery",
      "contactEmail": "avery@example.com",
      "createdAt": "2025-03-18T17:30:00Z",
      "eventId": 1234,
      "workflowStage": "draft_event_admin"
    }
  ]
}
```

**Possible errors**

- `LLM_SUBMISSION_INVALID_STAGE` – bad `stage` filter; check `/api/llm/metadata/stages`.
- `LLM_SUBMISSION_LIST_FAILED` – backing Supabase query failed (see hint for details).

**Example**

```jsonc
{
  "tool": "list_submissions",
  "input": {
    "search": "Lady Justice",
    "activeOnly": true
  }
}
```

### `get_submission`

Fetch a single submission snapshot. The payload mirrors the internal `/api/submissions/{id}/workflow-data` route, so you get:

- `submission` – core submission row + `formData` (now includes `guest_flow`), concept/example info, HubSpot flags, etc.
- `draftEventData`, `assessmentData`, `conceptData`, `questionsAndAnswers` – workflow context.
- `hubspotDeal`, `offerData`, `eventData` – linked external systems.
- `submissionContacts` – full contact objects (with profile + auth info).
- `linkedContacts` – simplified view kept for backwards compatibility.

**Inputs**

- `submissionId` (string UUID)

**Returns**

```jsonc
{
  "submission": {
    "id": "uuid",
    "restaurant_name": "Lady Justice",
    "contact_name": "Avery",
    "contact_email": "avery@example.com",
    "created_at": "2025-03-18T17:30:00Z",
    "event_id": 1234,
    "workflowState": "draft_event_admin",
    "status": "draft_event_admin",
    "hasHubspotDeal": true,
    "hubspotDealId": "123456789",
    "hubspotPipelineName": "Events",
    "hubspotStageName": "Planning",
    "hasCaseStudy": false,
    "guestFlow": "6:30pm - First 40 guests\n7:00pm - Next 40 guests",
    "formData": {
      "...": "...",
      "guest_flow": "6:30pm - First 40 guests\n7:00pm - Next 40 guests"
    }
  },
  "assessmentData": { "...": "..." },
  "conceptData": { "...": "..." },
  "draftEventData": {
    "guest_flow": "6:30pm - First 40 guests\n7:00pm - Next 40 guests",
    "...": "..."
  },
  "questionsAndAnswers": [
    { "question": "How many guests?", "answer": "120" }
  ],
  "hubspotDeal": {
    "dealId": "123456789",
    "pipelineName": "Events",
    "stageName": "Planning"
  },
  "offerData": { "...": "..." },
  "eventData": { "...": "..." },
  "submissionContacts": [
    {
      "id": "contact-link-id",
      "user_id": "user-uuid",
      "contact_role": ["owner"],
      "user_profiles": {
        "full_name": "Avery Justice",
        "hubspot_contact_id": "987654321"
      },
      "auth_users": {
        "email": "avery@example.com",
        "phone": "+1-555-0100"
      }
    }
  ],
  "linkedContacts": [
    {
      "submissionContactId": "contact-link-id",
      "userId": "user-uuid",
      "name": "Avery Justice",
      "email": "avery@example.com",
      "contactRole": ["owner"],
      "linkedAt": "2025-03-18T18:00:00Z"
    }
  ],
  "workflowId": "workflow-uuid",
  "stateData": { "chatgpt_link": "https://chat.openai.com/..." },
  "chatgptLink": "https://chat.openai.com/..."
}
```

**Possible errors**

- `LLM_SUBMISSION_ID_REQUIRED` – missing/blank `submissionId`.
- `LLM_SUBMISSION_NOT_FOUND` – invalid ID or removed submission.

### `duplicate_submission`

Clone an existing submission for quick retries. Copies form data, contacts, concept/example links, and workflow tree; clears event links, resets workflow to `draft_event_admin`, and returns a fresh submission snapshot.

**Inputs**: `submissionId`

**Returns**

```jsonc
{
  "submission": { /* same schema as get_submission */ }
}
```

**Possible errors**: `LLM_SUBMISSION_ID_REQUIRED`, `LLM_SUBMISSION_DUPLICATE_FAILED` (see message for which copy step failed).

### `create_submission`

Create a new submission record (and kick off its workflow). Always call `list_submissions` first to avoid duplicates.

**Inputs**

- `restaurantName` (required)
- `restaurantAddress` (optional, defaults to a placeholder)
- `contact` → `{ name, email, phone? }` (name + email required)
- `source` – identifier for the workflow/agent (`'n8n'`, `'internal-llm'`, etc.)
- `createdBy` – human-friendly run ID or operator badge for auditing
- `notes`, `metadata`, `formFields` (free-form records stored with submission)
- `sessionReplayLink` – optional URL
- `eventId` – numeric backend event ID to link immediately after creation

**Returns**

```jsonc
{
  "submissionId": "uuid",
  "workflowStage": "submission_received",
  "submission": { /* snapshot as in get_submission */ }
}
```

**Possible errors**

- `LLM_SUBMISSION_INVALID_PAYLOAD` – missing required fields; `hint` explains which one.
- `LLM_SUBMISSION_CREATE_FAILED` – downstream creation failure (see message).

### `link_submission_contact`

Link an existing HubSpot contact (and optional partner user) to a submission. If the link already exists, the response reflects its status without error.

**Inputs**

- `submissionId`
- `hubspotContactId` – HubSpot primary key (required)
- `contactRole` – optional array of role strings (e.g., `['owner', 'operator']`)
- `profileOverride` – optional `{ firstName, lastName, email, phone, company }` to sync profile details if HubSpot lacks them.

**Returns**

```jsonc
{
  "submissionId": "uuid",
  "contact": { /* linked contact summary */ },
  "linked": true,
  "imported": false,
  "message": "Linked contact owner to submission"
}
```

**Possible errors**: `LLM_SUBMISSION_ID_REQUIRED`, `LLM_CONTACT_ID_REQUIRED`, and the HubSpot-specific errors surfaced by `linkHubspotContactToSubmission` (`LLM_CONTACT_NOT_FOUND`, `LLM_CONTACT_SYNC_FAILED`, etc.).

### `link_submission_event`

Associate a backend (Rails) event record with a submission. Validates that the event exists, isn’t already linked elsewhere, and matches ownership rules from the backend.

**Inputs**: `submissionId`, numeric `eventId`

**Returns**

```jsonc
{
  "submissionId": "uuid",
  "eventId": 9876,
  "previousEventId": 1234
}
```

`previousEventId` helps audit changes if a submission already pointed at a different event.

**Possible errors**: `LLM_SUBMISSION_ID_REQUIRED`, `LLM_EVENT_ID_REQUIRED`, `LLM_EVENT_INVALID_ID`, `LLM_EVENT_NOT_FOUND`, `LLM_EVENT_VERIFICATION_FAILED`, `LLM_EVENT_LINK_FAILED`.

### `move_submission_stage`

Force-move a submission to any valid workflow stage (admins only). Use sparingly and provide a concise audit reason so humans can follow along later.

**Inputs**

- `submissionId`
- `targetStage` – one of the IDs listed above
- `reason` – short explanation stored in the workflow’s history
- `metadata` – optional object (e.g., `{ actorRunId, confidence }`)

**Returns**

```jsonc
{
  "submissionId": "uuid",
  "from": "draft_event_admin",
  "to": "waiting_launch"
}
```

**Possible errors**: `LLM_SUBMISSION_ID_REQUIRED`, `LLM_STAGE_REQUIRED`, `LLM_STAGE_REASON_REQUIRED`, `LLM_SUBMISSION_INVALID_STAGE`, `LLM_WORKFLOW_NOT_FOUND`, `LLM_STAGE_UPDATE_FAILED`.

### `list_submission_stages`

Discover legal workflow stages plus their allowed transitions. Feed this into your prompts so the agent knows what moves are legal.

**Returns**

```jsonc
{
  "stages": [
    {
      "id": "draft_event_admin",
      "label": "Draft Prep",
      "order": 4,
      "allowedTransitions": ["draft_event"]
    },
    ...
  ]
}
```

No inputs required. Errors are rare—if Supabase is unavailable the MCP server will return a generic `LLM_MCP_ERROR` with the upstream hint.

All commands proxy to the locked-down HTTP API, so removing the `mcp-server/` folder detaches the MCP integration cleanly.***