# trader_mcp operator manual

This server exposes the **showman2** market-data API to an MCP client (Claude).
It is a read-only data and account-settings bridge: it does not place trades, move
money, or change market data.

**Read this entire manual once before using any data tool.** Call the
`read_manual` tool at the start of a conversation, then use the tools described
below. If you are unsure which tool to use, re-read the relevant section.

---

## 1. Access tiers (important)

The MCP server forwards your HTTP authentication to showman2. There are three
effective states:

| State | How it is reached | What you can see |
|---|---|---|
| **anonymous / free** | No `Authorization` header | Only symbols on showman2's static free-ticker allowlist. News only if older than 7 days. Recommendations are filtered to free symbols. No settings, no MOTD, no `/me`. |
| **authenticated free** | Valid Basic credentials, but the account has no active subscription | Identical data limits to anonymous. The connection is authenticated, so `get_me`, `get_settings`, `update_settings`, and `get_motd` can work, but market data remains free-tier. |
| **paid / admin** | Valid Basic credentials for an active subscription or an admin account | Full ticker universe, news within the last 5 years, unfiltered recommendations, settings, MOTD. |

Every data tool response contains an `_access` object:

```json
"_access": {
  "authenticated": true,
  "email": "you@example.com",
  "role": "user",
  "full_access": true,
  "tier": "paid",
  "note": "Full showman2 data is available."
}
```

Use `_access.full_access` to decide whether a missing ticker/news item is really
missing or merely hidden by the free tier. **Do not** describe free-tier
truncation as "no data exists".

`_access` reflects the cached login. showman2 re-checks the subscription on every
upstream request, so the **data** is always correctly gated even if `_access` is
briefly stale. Call `get_me` for the freshest access check; its top-level
`full_access` comes directly from showman2.

### How credentials are configured

The client sends a fixed HTTP header to the MCP server:

```http
Authorization: Basic base64(showman2_email:showman2_password)
```

For Claude web, this is normally entered once in the custom connector's
**Request headers** setting. The MCP server exchanges the Basic credentials for
a showman2 JWT, caches that JWT in memory, and uses it for upstream
calls. The password is never returned to the model and should never be pasted
into the chat.

- `401` means the header is missing/malformed or showman2 rejected the
  credentials.
- `403` with "email is not verified" means the showman2 account must verify its
  email before paid access.
- `429` means showman2's login or data rate limit was reached.

If the connector sends no `Authorization` header, everything still works, but
only the free tier is visible. Requests for paid symbols return `404`, not a
permission error, so that the existence of paid symbols is not leaked.

---

## 2. Tool catalogue

| Tool | Auth | Purpose |
|---|---|---|
| `read_manual` | none | Returns this manual. Call it first. |
| `list_tickers` | optional | Paginated ticker summaries: symbol, company name, estimation, evaluation, certainty. |
| `search_tickers` | optional | Search symbols and company/product keywords. |
| `get_ticker` | optional | Full detail for one symbol: estimates, reasons, price CSV, EPS CSV, news CSV. |
| `list_news` | optional | Paginated news articles, optionally filtered by ticker and title text. |
| `get_news` | optional | One full news article by id. |
| `list_recommendations` | optional | Admin-curated strategy profiles (`balanced`, `aggressive-growth`, `risk-hater`). |
| `get_me` | **required** | Your email, role and current `full_access` flag. |
| `get_settings` | **required** | Your private settings JSON blob. |
| `update_settings` | **required** | Shallow-merge top-level fields into your settings. |
| `get_motd` | **required** | Current message of the day and whether you acknowledged it. |
| `acknowledge_motd` | **required** | Record that you saw the current MOTD. |

All tools return JSON (except `read_manual`, which returns Markdown text). Error
results are marked as tool errors and contain an `error`, `status`, `hint`, and
`_access` object.

---

## 3. Ticker summaries and how to read the scores

`list_tickers`, `search_tickers`, and `list_recommendations` return items shaped
like:

```json
{
  "ticker": "AAPL",
  "name": "Apple Inc. Common Stock",
  "estimation": 294.46548585474,
  "estimation_evaluation": 0.0290392875669898,
  "certainty": 68.7593216607537
}
```

Fields:

- **`ticker`** — exchange symbol, usually uppercase. Some symbols contain a dot
  (for example `BRK.B`).
- **`name`** — company name from showman2's keyword file. May be `null`.
- **`estimation`** — the model's estimated price target in **USD per share**.
  This is a price, not a return. May be `null` when the estimation engine has no
  result for that symbol.
- **`estimation_evaluation`** — the percentage difference between `estimation`
  and the model's reference price (`base_price`), expressed in **percent
  points**. The formula is:

  ```text
  estimation_evaluation = 100 * (estimation / base_price - 1)
  ```

  So `0.029` means approximately **+0.029%**, and `-10.46` means approximately
  **-10.46%**. Positive means the estimate is above the reference price; negative
  means below. It is not a guaranteed return and it is not a probability.
- **`certainty`** — model confidence on a **0–100** scale. Higher is more
  confident. It is not a probability and should not be multiplied by the
  estimated return. May be `null`.

**Ranking guidance.** When comparing symbols, do not rank on `estimation` alone.
A sensible default is:

1. Filter to rows where `estimation` and `certainty` are non-null.
2. Prefer a combination of positive `estimation_evaluation` and high `certainty`.
3. Treat extreme `estimation_evaluation` values as potential model outliers;
   inspect `get_ticker` reasons before presenting them as opportunities.
4. Mention that these are model estimates, not investment advice.

### Sorting and paging

`list_tickers` supports:

- `page` — 1-based, 40 items per page.
- `sort` — `ticker` (default), `estimation`, `estimation_evaluation`, or
  `certainty`.
- `order` — `asc` (default) or `desc`.

When sorting by a numeric column, rows with `null` values are still present.
Page until you have enough candidates, but do not loop indefinitely: stop when a
page returns fewer than 40 items.

### Search

`search_tickers` takes `q` and does a case-insensitive substring match against
the symbol and its keywords (company names, products, alternate names). It
returns at most 50 items:

```json
{ "query": "apple", "count": 1, "items": [ { "ticker": "AAPL", "keywords": ["Apple Inc. Common Stock"] } ] }
```

Search results are symbol discovery only — they do **not** include prices or
estimates. Follow up with `get_ticker` or `list_tickers` for those fields.

---

## 4. Full ticker detail

`get_ticker` takes:

- `ticker` (**required**) — symbol; case-insensitive.
- `range` — `1W`, `1M`, `1Y` (default), `2Y`, or `5Y`. Controls the time window
  for `prices`, `news`, and `eps`. Unknown values fall back to `1Y`.
- `columns` — optional comma-separated price columns to include. `date` is
  always included. Valid names: `date`, `open`, `high`, `low`, `close`,
  `adj_close` (default), `volume`, `dividend`, `split`.

The response shape is:

```json
{
  "ticker": "AAPL",
  "name": "Apple Inc. Common Stock",
  "estimation": 294.46548585474,
  "estimation_evaluation": 0.0290392875669898,
  "estimation_reason": { "...": "..." },
  "certainty": 68.7593216607537,
  "certainty_reason": { "...": "..." },
  "news": "date,news_id,relevance_score,sentiment_score,sentiment_label\n...",
  "prices": "date,adj_close\n...",
  "eps": "report_date,period_end,eps_actual,eps_estimate\n...",
  "_access": { "...": "..." }
}
```

### 4.1 `prices` — CSV string, no header row

`prices` is a **string containing CSV**, not a JSON array. It has **no header
row**. The canonical column order is:

```text
date,open,high,low,close,adj_close,volume,dividend,split
```

`columns` projects the row to the requested subset. For example,
`columns="close,volume"` produces:

```text
date,close,volume
2026-01-02,185.64,45234567
...
```

- `date` is `YYYY-MM-DD`.
- Prices are USD.
- `adj_close` is adjusted for splits/dividends; prefer it for return
  calculations.
- `volume` is shares.
- `dividend` is the cash dividend for that day (often `0.0000`).
- `split` is the split factor (often `1.0`).
- Rows are in ascending date order after filtering.
- The `1W`/`1M`/`1Y`/`2Y`/`5Y` filter is based on the row date, relative to
  today.

If you need a simple recent price, use the last row. For returns, compute from
`adj_close`, not `close`.

### 4.2 `eps` — CSV string, no header row

`eps` is a CSV string with four columns:

```text
report_date,period_end,eps_actual,eps_estimate
```

Example:

```text
2026-01-29,2025-12-31,1.89,2.10
```

- `report_date` — when the result was reported (`YYYY-MM-DD`).
- `period_end` — fiscal period end (`YYYY-MM-DD`).
- `eps_actual` — reported earnings per share.
- `eps_estimate` — consensus estimate at the time.
- Rows are ascending by `report_date` and filtered by the same `range` window.
- Values may be negative. Do not assume every row is a quarter; use the dates.

A simple surprise calculation is `eps_actual - eps_estimate`. Only describe it as
an earnings surprise when both values are present.

### 4.3 `news` (inside ticker detail) — CSV string, no header row

The ticker-level `news` blob is a CSV string with five columns:

```text
date,news_id,relevance_score,sentiment_score,sentiment_label
```

Example:

```text
2026-06-25,20260625T021058_05c3782b,0.648828,0.117431,Neutral
```

- `date` — publication date (`YYYY-MM-DD`).
- `news_id` — pass this to `get_news` for the full article.
- `relevance_score` — 0–1 relevance of the article to this ticker.
- `sentiment_score` — roughly -1 (bearish) to +1 (bullish) for this ticker.
- `sentiment_label` — provider label such as `Neutral`, `Bullish`,
  `Somewhat-Bullish`, `Bearish`, `Somewhat-Bearish`.
- Rows are filtered by `range` and are not necessarily sorted; sort by `date` in
  your own analysis if order matters.
- The ticker-level blob is only a pointer table. It does **not** contain the
  article title or summary; call `get_news` for that.

### 4.4 `estimation_reason` — JSON object

This explains how the estimated price was produced. Common keys:

| Key | Meaning |
|---|---|
| `base_price` | Reference price used for the estimate. |
| `baseline_ema` | Exponential moving average baseline. |
| `gravity_anchor` | Anchor price after the EMA gravity step. |
| `ema_gravity_weight` | Weight of the EMA gravity component. |
| `eps_impact_pct` | EPS contribution, in percent. |
| `news_impact_pct` | News-sentiment contribution, in percent. |
| `pe_balancing_impact_pct` | P/E balancing contribution, in percent. |
| `trend_adjustment_pct` | Trend adjustment, in percent. |
| `final_estimation` | Final estimated price; should equal `estimation`. |
| `cap_applied` | `"-"` when uncapped, otherwise a note such as `"Capped to +1.2*STD (+1.44)"`. |

Use these as **diagnostics**. A large `news_impact_pct` or
`pe_balancing_impact_pct` means the estimate is sensitive to that component.
`cap_applied` means the raw model output was clamped; treat the estimate as
bounded rather than precise. The percentages are not additive guarantees and may
use different denominators.

### 4.5 `certainty_reason` — JSON object

This explains the certainty score. Typical keys:

```json
{
  "all": 68.75932166075373,
  "ema": 66.2280701754386,
  "ema_eps": 68.75932166075373,
  "ema_eps_news": 68.75932166075373,
  "ema_news": 66.2280701754386
}
```

These are sub-scores on the same 0–100 scale for different feature combinations.
Higher is more confident. A large gap between sub-scores means the confidence is
sensitive to which inputs are included; mention that if it matters.

---

## 5. News

### 5.1 `list_news`

Parameters:

- `page` — 1-based, 20 items per page.
- `tickers` — optional comma-separated symbols. An article matches if at least
  one symbol appears in its `ticker_sentiment` list.
- `q` — optional case-insensitive substring matched against the article
  `title`.

Response:

```json
{
  "page": 1,
  "per_page": 20,
  "items": [
    {
      "id": "20260625T021058_05c3782b",
      "date": "2026-06-25 02:10:58",
      "content": "{\"title\":\"...\",\"summary\":\"...\", ...}"
    }
  ],
  "_access": { "...": "..." }
}
```

**Critical:** `items[].content` is a **JSON string**, not a JSON object. Parse it
twice: first parse the list item, then parse the `content` string. If parsing
fails, treat the raw string as text.

Anonymous and authenticated-free callers only see articles older than **7 days**.
Paid callers see everything within the last **5 years**. Articles older than 5
years are not served. If a specific id returns `404` for a free-tier caller, it
may simply be too fresh.

News list items are ordered newest first (`date DESC, id DESC`).

### 5.2 `get_news`

Takes `id` and returns one item in the same shape. The same freshness and
5-year rules apply. Use the `news_id` values from `get_ticker`'s `news` CSV or
from `list_news`.

### 5.3 Parsed article content

After parsing the `content` JSON string, expect:

```json
{
  "title": "Article title",
  "summary": "Short summary",
  "url": "https://...",
  "source": "New York Post",
  "source_domain": "New York Post",
  "time_published": "20260625T021033",
  "authors": ["Brian Gallagher"],
  "banner_image": "https://... or null",
  "category_within_source": "General",
  "overall_sentiment_label": "Somewhat-Bullish",
  "overall_sentiment_score": 0.33551,
  "ticker_sentiment": [
    {
      "ticker": "YELP",
      "relevance_score": "1.000000",
      "ticker_sentiment_score": "0.312804",
      "ticker_sentiment_label": "Somewhat-Bullish"
    }
  ],
  "topics": [
    { "topic": "retail_wholesale", "relevance_score": "0.844358" }
  ]
}
```

`time_published` is a compact `YYYYMMDDTHHMMSS` string. `ticker_sentiment` scores
and topic relevance scores are often strings, not numbers; convert them when
calculating. Sentiment scores are provider estimates, not facts.

When summarising news:

- Cite the title, source, date and URL.
- Distinguish the article's overall sentiment from per-ticker sentiment.
- Do not imply that a news item caused a price move unless the article says so.
- Prefer `relevance_score` to filter out tangential mentions.

---

## 6. Recommendations

`list_recommendations` returns an admin-curated ordered list for a strategy
profile:

- `profile` — defaults to `balanced`. Known profiles in the current deployment:
  `balanced`, `aggressive-growth`, `risk-hater`. The list is dynamic; an unknown
  profile falls back to the default profile.
- `page` — 1-based, 40 items per page.

Response items have the same summary shape as `list_tickers` (`ticker`, `name`,
`estimation`, `estimation_evaluation`, `certainty`). The order is intentional and
should be preserved when presenting the list. A recommended ticker that is
missing from the ticker database is still returned with `null` numeric fields;
do not treat that as an error.

For free-tier callers, recommendations are filtered to the free allowlist, so a
profile may contain far fewer rows than it does for a paid account. If a profile
looks unexpectedly short, check `_access.full_access`.

Recommendations are **not** personalised advice and may be stale. Always pair
them with current `get_ticker` data and state the profile name and date context.

---

## 7. Account, settings, and MOTD

### `get_me`

Returns:

```json
{ "email": "you@example.com", "role": "user", "full_access": true }
```

Use this to confirm which account the connector is using and whether paid access
is active. If `full_access` is false, data tools will apply free-tier limits even
though the request is authenticated.

### `get_settings`

Returns the caller's settings JSON object. The known fields are:

| Field | Type | Notes |
|---|---|---|
| `nickname` | string | Display name. |
| `phone_number` | string | Optional contact number. |
| `discord_hook` | string | Optional Discord webhook URL. Treat as a secret; do not repeat it unnecessarily. |
| `positions` | array | List of objects, typically `{ "ticker": "AAPL", "buy_price": 180.0, "estimation": 210.0 }`. |
| `preferred_strategy` | string | Defaults to `"default"`. |

Settings are private to the authenticated user. They are an opaque JSON blob:
showman2 never uses them to filter data. Do not expose them to other users.

### `update_settings`

The tool argument is an **object of top-level fields to merge**, not a nested
`settings` wrapper. For example:

```json
{
  "nickname": "Alex",
  "preferred_strategy": "balanced",
  "positions": [
    { "ticker": "AAPL", "buy_price": 180.0, "estimation": 210.0 }
  ]
}
```

showman2 performs a shallow merge: supplied top-level keys replace the old
values; omitted keys are kept; a `null` value overwrites a key. The response is
the merged object. To add a position, fetch the current settings first and send
the complete `positions` array, because arrays are replaced wholesale.

Settings are capped at 64 KiB by showman2. Do not store large documents there.

### `get_motd` / `acknowledge_motd`

`get_motd` returns:

```json
{
  "message": {
    "date_effective": "2026-10-17",
    "escapable": true,
    "text": "Welcome! ..."
  },
  "acknowledged": false
}
```

`message` is `null` when no MOTD is configured. `acknowledged` is true when the
current user has seen this version or a newer one. `escapable` indicates whether
the UI may offer a dismiss control; when false, the message should only be
dismissed with an explicit acknowledgement.

Call `acknowledge_motd` only after showing the message to the user and only if
the user acknowledges it. Do not acknowledge silently on the user's behalf.

---

## 8. Errors, limits, and data freshness

### Common upstream errors

| Status | Meaning | What to do |
|---|---|---|
| `401` | Bad/missing credentials, expired JWT | Check the connector's Basic header. Do not ask for the password in chat. |
| `403` | Email not verified, or action requires paid access | Tell the user to verify the showman2 email / upgrade. |
| `404` | Symbol or news id not found, **or hidden by the free tier** | Check `_access.full_access` before concluding it does not exist. |
| `429` | showman2 rate limit | Wait and reduce request volume. Do not retry in a tight loop. |
| `5xx` / network | showman2 unavailable | Report the outage; retry later. |

### Pagination

- Tickers: 40 per page.
- News: 20 per page.
- Recommendations: 40 per page.

A page returns fewer than `per_page` items only on the last page. Stop paging
when the item count is less than `per_page`; do not request hundreds of pages
speculatively.

### Data freshness and correctness

- showman2 distributes data prepared by its admin; it is not a live exchange
  feed. Check the dates in the response before calling something "current".
- News may lag the publication time shown in `time_published`.
- Ticker rows may have `null` estimation/certainty values. Handle nulls; do not
  fabricate numbers.
- Prices and estimates are in USD unless explicitly stated otherwise.
- Free-tier news is delayed by 7 days; paid news goes back up to 5 years. Do not
  describe a 7-day delay as a bug.
- The free-ticker allowlist is static and controlled by showman2. Discover it
  with `list_tickers` rather than assuming a symbol is free.

### Security and privacy

- Never ask a user to paste their showman2 password into the chat.
- Never print, echo, or summarise the `Authorization` header.
- Treat `get_settings` data (phone number, Discord hook, positions) as private.
- Do not attempt to call showman2 admin endpoints; this MCP server does not
  expose them.

---

## 9. Recommended workflows

### Discover ideas

1. `list_recommendations` with `profile: "balanced"` (or the user's preferred
   strategy).
2. `get_ticker` for the strongest candidates, with `range: "1Y"` and
   `columns: "adj_close"`.
3. Inspect `estimation_reason`, `certainty_reason`, and recent ticker-level news.
4. `get_news` for the most relevant `news_id` values.

### Research one symbol

1. `search_tickers` if you only know the company name.
2. `get_ticker` with the desired `range`.
3. Parse `prices`, `eps`, and `news` CSV strings.
4. Read `estimation_reason` and `certainty_reason`.
5. Fetch full articles with `get_news` for the highest-relevance news ids.

### Daily news digest

1. `list_news` page 1 (paid access required for the freshest items).
2. Optionally filter by `tickers` or `q`.
3. Parse each `content` JSON string and summarise title, source, date, overall
   sentiment, and ticker-specific sentiment.

### Check account access

1. `get_me`.
2. If `full_access` is false, explain that only free-tier data is visible and
   how to configure paid Basic credentials in the connector.

### Update positions

1. `get_settings`.
2. Modify the `positions` array locally.
3. `update_settings` with the complete `positions` array and any other changed
   top-level fields.
4. Confirm the merged response.

---

## 10. Interpretation rules for the model

- Always distinguish **model estimates** from **market facts**. `estimation` is a
  target price produced by a model; `certainty` is model confidence.
- Never present `estimation_evaluation` as a guaranteed return, probability, or
  price. It is a percentage gap to the reference price.
- When a value is `null`, say so. Do not infer it from other fields.
- When data is free-tier-limited, say "not visible on the free tier" rather than
  "does not exist".
- Mention the time range, profile name, and access tier when they affect the
  answer.
- Do not give personalised financial advice. Frame outputs as data analysis and
  include the relevant caveats.
- If a tool returns an error, inspect `status` and `hint` before retrying. Do not
  retry a `401` or `403` with the same credentials; do not retry a `429` in a
  tight loop.
