LOADING
Preparing route…
SparkRaise is resolving the next route and hydrating the current company or dashboard surface.
SparkRaise is resolving the next route and hydrating the current company or dashboard surface.
Canonical phase-1 tool contracts for MCP and their REST mirrors. This view is sourced from the repository reference doc so the product docs stop linking to a dead markdown path.
# MCP / API Surface
Same contract available via:
- **MCP** — `@modelcontextprotocol/sdk` server; stdio + streamable-http transports
- **REST** — `/api/v1/<tool_name>` on the web app, identical zod schemas
All inputs/outputs are JSON. Errors are `{ ok: false, error: <code>, message?, details? }`.
Every call is logged to `McpCall` (or `SearchQuery` for searches). Every authed call includes `account_id` and `token_id` for attribution.
---
## Authentication
- **No unauthenticated MCP.** Every tool call requires a bearer token.
- **Token issuance** requires a verified email on an Account.
- **Token scopes:**
- `read` — default on signup. Public reads + own-account reads.
- `write_listing` — separate explicit mint. Required for any listing mutation or document upload.
- `admin` — role-gated; only Accounts with `role='admin'` can mint.
- Future disclosure split: `docs/DISCLOSURE-TIERS.md` reserves `read_verified` for verified-investor
access and a separate per-listing founder-approved grant for diligence material. Public tools in
this document should continue to expose public-tier fields only.
- **Rate limits:**
- `free` tier: 60 search/hr, 1000 search/day; 30 writes/hr
- `pro` tier: 600/hr, 20k/day; 200 writes/hr
- `unlimited`: no per-window cap, fair-use monitoring
- Limits are per-token; `429` with `Retry-After` on exhaustion.
**Header:** `Authorization: Bearer sr_live_...`
- **stdio MCP**: bearer token supplied once via `SPARKRAISE_TOKEN` in the server environment.
- **streamable-http MCP**: bearer token sent on every `/mcp` request. Discovery card at `/.well-known/mcp.json`; protected-resource metadata at `/.well-known/oauth-protected-resource/mcp`.
- **Marketplace OAuth clients**: public clients self-register at `/api/oauth/register`, then complete OAuth 2.1 authorization-code + PKCE. Tokens can be revoked at `/api/oauth/revoke`. Phase 1 supports `token_endpoint_auth_method = none` only.
---
## Resources and prompts
The MCP server now exposes product-copy resources and named prompts in addition to tools.
### `resources/list` / `resources/read`
Available resource families include:
- `sparkraise://guide/listing-rubric.md`
- `sparkraise://guide/investor-playbook.md`
- `sparkraise://guide/publishing-workflow.md`
- `sparkraise://taxonomy/categories.md`
- `sparkraise://policy/regulatory.md`
- `sparkraise://schema/listing.json`
- `sparkraise://example/listing/<slug>.json`
These are static, reviewed reference assets designed to help an agent choose better arguments before it calls a tool. They are product copy, not runtime user data.
### `prompts/list` / `prompts/get`
Named prompt templates currently include:
- `publish_company_profile`
- `investor_brief`
- `monitor_portfolio`
These prompts are cookbook-style workflow starters. They do not execute tools themselves; they tell the client-side agent which resources to read, which tools to call, and in what order.
---
## Public / Read (token scope: `read`)
### `list_sectors`
*(Legacy — kept for backwards-compat. Prefer `list_categories` with `kind='sector'`.)*
Returns the canonical sector list for filtering UIs.
```ts
input: {}
output: { sectors: string[] }
```
### `list_stages`
```ts
input: {}
output: { stages: CompanyStage[] }
```
### `list_categories`
Returns the controlled taxonomy. Callers should cache this (vocabulary changes rarely; `updatedAt` on the response is suitable for cache keys).
```ts
input: {
kinds?: ('sector' | 'vertical' | 'business_model' | 'audience')[],
parent_slug?: string, // return only children of this parent
active_only?: boolean // default true
}
output: {
categories: Array<{
slug: string,
name: string,
kind: 'sector' | 'vertical' | 'business_model' | 'audience',
parent_slug?: string,
description?: string,
aliases: string[]
}>,
updated_at: string
}
```
### `suggest_categories`
Given a candidate listing (name + description + website URL + optional founder LinkedIn), returns ranked category slug suggestions. For seller agents preparing an `upsert_listing` call — lets them reach for the vocabulary before submitting, rather than failing validation on submit.
```ts
input: {
name: string,
one_liner?: string,
description?: string,
website?: string,
max_per_kind?: number // default 3
}
output: {
suggestions: Array<{
slug: string,
kind: 'sector' | 'vertical' | 'business_model' | 'audience',
confidence: number, // 0..1
rationale: string // short reason — for agent to show founder
}>
}
```
Implementation is inline and heuristic-first in phase 1: alias/keyword matching plus a few fast business-model / audience hints. No worker hop, no async MCP polling.
### `validate_categories`
Given a set of category slugs, returns which are valid, which are deprecated, and nearest matches for unknowns. Seller agents should call this before `upsert_listing` as a pre-flight, especially when the founder typed category names by hand.
```ts
input: {
slugs: string[]
}
output: {
valid: string[],
deprecated: Array<{ slug: string, replacement?: string }>,
unknown: Array<{ input: string, nearest: Array<{ slug: string, score: number }> }>
}
```
### `search_companies`
The core public discovery tool. Returns published, fresh/aging listings using public-tier fields
only.
```ts
input: {
query?: string, // weighted full-text search over name + one_liner + description; supports quoted phrases and prefix terms
sectors?: string[], // legacy — resolves to category slugs with kind='sector'
categories_include?: string[], // canonical M:N filter — all listings matching ANY of these category slugs
categories_exclude?: string[], // exclude listings matching any of these slugs
stages?: CompanyStage[],
hq_country?: string[], // ISO-3166 alpha-2
business_model?: string[], // category slugs with kind='business_model'
raising?: boolean, // has active Raise
signals?: string[], // badge filters
verified_only?: boolean,
sort?: 'relevance' | 'recent' | 'growth' | 'arr' | 'freshness',
limit?: number, // default 25, max 100
cursor?: string
}
output: {
results: Array<{
slug, name, logo_url, hq_country, hq_city?, stage, one_liner,
sector: string[],
is_raising, trust_tier, freshness_state, trust_signals?,
fit_hints?: string[] // matched-on fields (for UI highlighting)
}>,
total_approx: number,
next_cursor?: string
}
```
Verified-investor filters such as detailed metrics, team size, and financial thresholds are reserved
for a future `read_verified` surface rather than the public search contract.
### `get_company`
Full public company profile.
```ts
input: { slug: string }
output: {
slug, name, logo_url, website, hq_country, hq_city, founded_year,
stage, sector, sub_sector, business_model, one_liner, long_description,
hiring_open, trust_tier, freshness_state, trust_signals?,
raise?: { status },
updated_at
}
```
Founder identities, detailed metrics, valuations, committed-round progress, and private document
references sit outside the public contract and belong to future verified-investor or
founder-approved surfaces.
### `get_raising_now`
Convenience — equivalent to `search_companies` with `raising=true`.
```ts
input: { limit?: number }
output: { results: [...], total_approx: number }
```
---
## Seller / Listing Writes (token scope: `write_listing`)
Writer tools require the token's Account to own the referenced `listing_id`, unless creating a new listing.
### `upsert_listing`
Create or update the caller's listing.
```ts
input: {
slug?: string, // if null, server generates from name
type?: 'company', // default 'company'
name: string,
website?: string,
hq_country: string,
hq_city?: string,
founded_year?: number,
one_liner?: string,
long_description?: string,
business_model?: string,
stage?: CompanyStage,
sector?: string[], // legacy compatibility only — not canonical on write
sub_sector?: string[], // legacy compatibility only — not canonical on write
categories?: string[], // canonical — category slugs (M:N, any kind). Validate via `validate_categories` before calling.
headcount?: number,
hiring_open?: boolean,
currency?: string,
founders?: Array<{ name, role?, linkedin_url?, twitter_url?, bio? }>,
logo_url?: string
}
output: {
listing_id, slug, approval_status, trust_tier, preview_url
}
```
New listings land in `approval_status='draft'`. Material-field edits on `published` listings move them back to `pending_review`.
If `categories` contains unknown or deprecated slugs, the tool returns `validation_error` and the caller should run `validate_categories` first. Legacy `sector` / `sub_sector` inputs are accepted for backwards compatibility but are no longer the canonical write path.
### `add_metrics`
Append a monthly metrics row.
```ts
input: {
listing_id: string,
period: string, // YYYY-MM
mrr?, arr?, growth_mom?, growth_yoy?, burn?, runway_months?, gross_margin?, customers?: number,
currency?: string,
note?: string
}
output: { metric_id, listing_id, period }
```
Idempotent on (listing_id, period) — re-upsert updates existing.
### `open_raise`
```ts
input: {
listing_id: string,
amount_target: number, currency: string,
instrument: RaiseInstrument,
valuation_pre?, valuation_post?, min_ticket?: number,
close_target_date?: string,
pitch_deck_storage_key?: string,
use_of_funds?: string,
highlights?: string[]
}
output: { raise_id, status: 'open' }
```
Only one open raise per listing. Moves CompanyProfile.is_raising = true.
### `update_raise`
```ts
input: { raise_id, ...partial, pitch_deck_storage_key?: string }
output: { raise_id, status }
```
### `close_raise`
```ts
input: { raise_id, closed_reason?: 'completed' | 'cancelled' | 'paused' }
output: { raise_id, status }
```
### `confirm_listing_current`
One-click freshness bump. No field changes, just resets `last_refreshed_at`.
```ts
input: { listing_id }
output: { listing_id, last_refreshed_at, freshness_state }
```
### `submit_for_review`
Moves `draft` → `pending_review`. Validates required trust evidence against the raise-size policy (see MODEL.md § Review-intensity policy).
```ts
input: { listing_id, cover_note?: string }
output: { listing_id, approval_status: 'pending_review', required_trust_tier }
```
Error: `precondition_failed` with `{ required_trust_tier, missing_evidence: TrustSignalKind[] }` when the listing's current trust evidence is below what the raise size demands. Seller agent should follow up with `upload_listing_document` and/or prompt the founder for manual verification steps, then retry.
### `withdraw_from_review`
Moves `pending_review` → `draft`.
```ts
input: { listing_id }
output: { listing_id, approval_status: 'draft' }
```
---
## Documents (token scope: `write_listing`)
### `upload_listing_document`
Accepts a file via base64 or a pre-signed upload flow.
```ts
input: {
listing_id: string,
doc_type: DocType,
filename: string,
mime_type: string,
content_base64?: string, // for ≤10MB
request_upload_url?: boolean, // returns PUT url for client-side upload of larger files
note?: string
}
output: {
document_id,
upload_url?, // if request_upload_url
sha256, // null for presign flow until upload completes
size_bytes, // null for presign flow until upload completes
received_at
}
```
Exactly one of `content_base64` or `request_upload_url=true` must be provided.
### `list_my_documents`
```ts
input: { listing_id }
output: {
documents: Array<{
id, doc_type, filename, mime_type?, size_bytes,
uploaded_at, reviewed_at?, reviewer_notes?, download_url
}>
}
```
### `delete_document`
Only allowed while listing is in `draft` or after reviewer has explicitly flagged re-upload.
```ts
input: { document_id }
output: { document_id, deleted: true }
```
---
## Monitoring (token scope: `write_listing` for add/remove; `read` for health)
### `add_monitoring_target`
Adds an endpoint and synchronously verifies it.
```ts
input: {
listing_id: string,
platform: MonitoringPlatform,
url: string
}
output: {
target_id,
platform,
url: string, // canonicalised
resolved_name?: string,
suggested_feed_url?: string,
health_state: 'healthy' | 'pending' | 'broken',
verification: {
ok: boolean,
reason?: 'profile_not_found' | 'blocked' | 'timeout' | 'rate_limited',
detail?: string
}
}
```
Phase 1: scaffolded, verification actually runs; signal emission deferred.
### `repair_monitoring_target`
```ts
input: { target_id, new_url: string }
output: { target_id, ...same as add }
```
Resets `consecutive_failures`, re-verifies.
### `remove_monitoring_target`
```ts
input: { target_id }
output: { target_id, removed: true }
```
### `list_monitoring_targets`
```ts
input: { listing_id }
output: { targets: Array<{ id, platform, url, health_state, last_checked_at, last_ok_at, consecutive_failures }> }
```
### `get_monitoring_health`
Summary for dashboards.
```ts
input: { listing_id }
output: { healthy: number, degraded: number, broken: number, deprecated: number, last_overall_check: timestamp }
```
---
## Buy-side / Investor (token scope: `read` sufficient)
### `watch`
```ts
input: { listing_id, note?: string }
output: { watchlist_id, added_at }
```
### `unwatch`
```ts
input: { listing_id }
output: { removed: true }
```
### `list_watchlist`
```ts
input: {}
output: { items: Array<{ listing_id, slug, name, logo_url, added_at, note? }> }
```
### `get_signals` *(phase 2 — stub in phase 1)*
```ts
input: { listing_id, since?: string, limit?: number }
output: { signals: Array<{ id, type, severity, title, payload, detected_at }>, total: number }
```
Phase 1 returns `{ ok: false, error: 'not_implemented', message: 'Signals ship in phase 2' }`.
### `save_search`
```ts
input: { name: string, filters: SearchCompaniesInput, alert_frequency?: 'off'|'daily'|'weekly' }
output: { saved_search_id }
```
### `run_saved_search`
```ts
input: { saved_search_id }
output: { results, total_approx, last_run_at }
```
### `request_introduction`
One-way intro request. Persists the introduction row and enqueues `email.intro` in the same transaction; no on-platform thread.
```ts
input: {
listing_id,
short_note?: string // max 500 chars; forwarded once, not stored long-term
}
output: { introduction_id, sent_at }
```
Requires verified account. Rate-limited per investor per listing (1/day).
### `create_share_link`
Mint a short-URL for a listing. Any authenticated caller can share; the recipient page includes a sign-up CTA. Owner-shared links attribute to the owner; investor-shared links attribute to the investor (feeds referral analytics).
```ts
input: {
listing_id: string,
purpose?: 'owner_promo' | 'investor_fwd' | 'generic', // default 'generic'
expires_in_days?: number // optional, caps at 365
}
output: { short_id: string, short_url: string, expires_at?: string }
```
Short IDs mint with bounded retry-on-conflict; after repeated collisions the generator increases the ID length rather than looping forever.
---
## Admin (token scope: `admin`)
### `admin_list_pending_reviews`
```ts
input: { limit?, cursor? }
output: { items: Array<{ listing_id, name, owner, submitted_at, docs_count, required_trust_tier }>, next_cursor? }
```
### `admin_approve_listing`
```ts
input: { listing_id, notes?: string }
output: { listing_id, approval_status: 'published' }
```
### `admin_reject_listing`
```ts
input: { listing_id, reason: string }
output: { listing_id, approval_status: 'rejected', rejection_reason }
```
### `admin_set_trust_tier`
```ts
input: { listing_id, trust_tier: TrustTier, reviewer_notes?: string }
output: { listing_id, trust_tier }
```
### `admin_override_tier`
```ts
input: { listing_id, tier: ListingTier, reason: string }
output: { listing_id, tier }
```
### `admin_pause_listing` / `admin_unpause_listing` / `admin_archive_listing`
```ts
input: { listing_id, reason?: string }
output: { listing_id, approval_status }
```
### `admin_disable_account`
```ts
input: { account_id, reason: string }
output: { account_id, deleted_at, is_disabled: true }
```
Soft-deletes the account, revokes active sessions, revokes API tokens, and writes an `AuditLog` row with action `account.soft_delete`.
### `admin_impersonate`
```ts
input: { account_id, reason: string }
output: { account_id, token_id, prefix, raw_token, expires_at }
```
Mints a 15-minute bearer token that resolves as the target account for ownership checks while preserving the
admin actor in `AuditLog` via `actor_account_id` + `on_behalf_of_account_id`.
### `admin_get_metrics`
Read-only snapshot for the dashboard (backed by materialized views).
```ts
input: { window?: '24h' | '7d' | '30d' | '90d' }
output: { accounts_new, listings_status_counts, search_volume, mcp_calls, funnel, content_quality }
```
---
## Self / Account (token scope: `read`)
### `whoami`
```ts
input: {}
output: { account_id, email, name, role, scopes, rate_tier, listings_owned: number }
```
### `list_my_listings`
```ts
input: {}
output: { listings: Array<{ id, slug, name, approval_status, tier, trust_tier, freshness_state, last_refreshed_at }> }
```
### `get_my_analytics`
Seller-facing (premium+) aggregated analytics for one of my listings.
```ts
input: { listing_id, window?: '7d'|'30d'|'90d' }
output: {
views_total, views_by_source: { web, mcp, agent },
views_by_firm_type: { angel, vc, family_office, corporate, syndicate, other, anonymous },
search_appearances, watchlist_adds,
agent_queries_matched,
series: Array<{ date, views, agent_queries }>
}
```
Firm-type counts only; never firm names (privacy default).
---
## Error Codes
| Code | HTTP | Meaning |
|---|---|---|
| `validation_error` | 400 | zod parse failure |
| `auth_error` | 401 | missing/invalid token |
| `precondition_failed` | 412 | valid request blocked by unmet evidence/policy |
| `forbidden` | 403 | valid token, insufficient scope or ownership |
| `not_found` | 404 | target doesn't exist or not visible |
| `rate_limited` | 429 | token bucket empty |
| `conflict` | 409 | e.g. slug taken |
| `not_implemented` | 501 | phase 2 stub |
| `server_error` | 500 | anything else |
---
## Phase 1 Implementation Status (for overnight scaffold)
**Wire all tools as registered stubs.** Implementations:
| Tool | Phase 1 state |
|---|---|
| list_sectors / list_stages | ✅ real (enum) |
| list_categories / suggest_categories / validate_categories | ✅ real |
| search_companies / get_company / get_raising_now | ✅ reads seed + real data |
| whoami / list_my_listings | ✅ real |
| upsert_listing / add_metrics | ✅ real |
| open_raise / update_raise / close_raise | ✅ real |
| confirm_listing_current / submit_for_review / withdraw_from_review | ✅ real |
| upload_listing_document / list_my_documents / delete_document | ✅ real metadata + S3-compatible object storage + presigned URLs |
| add_monitoring_target (verify) | 🔧 real for website/rss/github; stub for linkedin/x/crunchbase |
| repair/remove/list/get_monitoring_health | ✅ real |
| watch/unwatch/list_watchlist | ✅ real |
| get_signals | 🔶 `not_implemented` |
| save_search / run_saved_search | ✅ real |
| request_introduction | ✅ real row write + transactional enqueue (worker still logs delivery payload) |
| create_share_link | ✅ real short-link mint with collision retry + graceful length escalation |
| admin_* | ✅ real |
| get_my_analytics | 🔧 real counts, synthetic where no data exists yet |
All tools present in schema / routing. Phase 1 stops before paid signal emission pipeline.