# MuniPro banker integration

Status: Active
Profile: `banker`, version 1. Dataset schema: `1.0`.

Start with the [Rogo OAuth contract](rogo-connection-guide.md), then discover
the account's tools. The [profile JSON](banker-tool-profile.json) contains the
reviewed tool descriptions, input schemas, annotations and access requirements.
It is a maximum catalog, not proof of an account's access or client acceptance.

`profile_version` identifies the banker profile. `profile_schema_sha256` is
SHA-256 over its `tools` array, sorted by tool name and serialized as ASCII JSON
with sorted object keys, comma/colon separators and no whitespace. It binds
the eleven reviewed tool schemas, descriptions, annotations and published
access policies without depending on the site-wide contract version. The
public profile and the kit's `references/banker-tool-profile.json` are identical.
For the live `tool_contract_version`, call `get_current_munipro_context`;
the reviewed versioned contracts are in `docs/architecture/mcp-tool-contracts/`.

## Profile and scopes

The initial profile has **11 tools**: six business tools and five support tools.
The James/Rogo acceptance ceiling is **11 tools**, with every discovered
tool required to belong to this profile. Assignment
is to a user/company OAuth principal. Rogo and the native MuniPro platform
assistant use that same principal. A staff flag or disabled persona curation
does not expand an assigned profile. Invalid assignment, company mismatch or
an unsupported current role refuses access. Scope, host, entitlement, borrower,
private-workspace and active-owner checks still apply at call and download time.

Request `catalog.read debt.read borrowers.request offline_access` for the
complete external banker workflow. Omit `borrowers.request` for read-only use.
The additional scope permits a human-approved coverage request; it cannot
verify, publish, edit or delete debt. Existing grants need fresh consent for
an additional scope. Do not request `model.run` merely to download datasets.

| Tool | Purpose and principal inputs | Scope / minimum tier |
|---|---|---|
| `list_verified_borrowers` | Find visible, verified borrowers; `state`, `q`, `limit`, `offset`. Use returned borrower ID as `issuer_id`. | `debt.read` / Free |
| `request_borrower_verification` | Preview a staff coverage request: required `name`, `state`; optional six-character `cusip6`, `category`. | `borrowers.request` / Professional; active owner and human approval |
| `get_debt_details_dataset` | Queue selected outstanding debt and calculation datasets as JSON: `issuer_id`, `datasets`, `options`. | `debt.read` / Pro (`pro`); active owner |
| `list_issuer_files` | List the issuer's current uploaded package and named attachments. | `debt.read` / Free |
| `get_issuer_file_download` | Resolve one listed `file_id` for the same `issuer_id`; returns authenticated download metadata. | `debt.read` / Free |
| `list_issuer_debt_documents` | Page indexed Official Statements and historical financial statements; `issuer_id`, `kind`, `limit`, `offset`. | `debt.read` / Free |
| `get_current_munipro_context` | Identify current access, profile and presentation recipes. | Authenticated MCP bearer; no tool-specific scope / Free |
| `get_munipro_navigation` | Discover the tools and workflows available to this account. | Authenticated MCP bearer; no tool-specific scope / Free |
| `get_job_status` | Poll a returned `mcp-job://` reference. | Original job's scope / entitlement |
| `cancel_job` | Cancel an owned job subject to its state and authority. | Original job's scope / entitlement |
| `fetch` | Read an exact returned reference or result page. Hidden underlying tools remain refused. | Underlying resource's requirements |

The profile's `access.scope_policy` explains how to interpret `required_scopes`.
For job status and cancellation, `async_job_type_dependent` means the original
job selects the scope and entitlement; it does not require every listed scope.
A banker dataset job uses `debt.read`, without `model.run` or `debt.write`.

The exact connected catalog can be smaller. Use `tools/list` after OAuth,
follow `nextCursor`, and obey each live `inputSchema`. MCP database IDs are
normally strings; HTTP path IDs are positive integers. `issuer_id` is MuniPro's
**Borrower ID**, not an internal credit/Issuer table ID, CUSIP or company ID.
Non-CUSIP bonds are supported through returned security IDs. Prefer
`securityIds` for mixed CUSIP/non-CUSIP populations; never use a blank CUSIP as
identity. The data guide describes non-CUSIP document-coverage limitations.

Borrower-list results include identity, debt totals, a canonical issuer citation
and the three relevant dataset/file/document actions. Follow page references
for the complete list (maximum 100 borrowers per page). A roster-restricted
principal sees only its current user/firm roster intersected with visible,
verified records; an empty roster returns zero rows and zero total count.

Start with `get_current_munipro_context(detail="summary")`; it includes a compact
skill pointer and recipe count. Only for presentation work, use `detail="full"`
for the 16 recipes, complete instructions and source/review
requirements in the tool result; native assistants do not need a separate web
fetch to read the packaged skill. Slide rendering still requires the host's
presentation tools or an admitted firm pitchbook workflow.

The `banker_skills` catalog also exposes four portable workflows: cited debt
briefs, source packs, borrower coverage and firm presentations/templates. Follow
the selected entry's `get_munipro_navigation` call with `area="banker-skill:<id>"`
to read just that workflow and its supporting references. This returns public
instructions, not a new tool grant or executed work. See the
[Rogo skills implementation guide](rogo-skills-guide.md),
[skill catalog](banker-skills.json) and [portable bundle](munipro-banker-skills.zip).

## JSON and file HTTP interface

`list_verified_borrowers` and GET `borrowers/` return compact borrower rows.
Rows keep their identity, names, `action_arguments` and minimal direct `refs`.
Each `entry_actions` string key selects response `row_actions`; bind the selected
template from row `action_arguments`: resolve `{"$row": "borrower_id"}` to
the typed value; in strings replace only literal `{borrower_id}` with that value.
The response also holds `row_ref_templates`, `row_ref_aliases` (former row
`*_ref` names to template keys) and `row_human_pages`. Bind those templates the
same way and leave other placeholders unresolved. Availability, scopes and
action descriptions remain on the shared templates. Follow `refs.next_page`
using `total_count`, `offset`, `limit` and `complete`; page size is capped at 25.

These dedicated routes accept the **same MCP OAuth access token** in the
`Authorization` header, with the same current access checks. This is an explicit
exception to the independent credentials of the older Assistant service API
and `/api/v1/` REST APIs. Keep the OAuth resource `https://munipro.com/mcp`.
Cookies alone do not authorize these banker routes. Never put bearer, access or
refresh tokens in URLs. Keep the signed locator in a returned download URL intact;
it does not replace the bearer.

Base: `https://munipro.com/api/assistant/v1/banker/`. Preserve trailing slashes.

| Method / relative path | Input | Result |
|---|---|---|
| GET `borrowers/` | Query `state`, `q`, `limit` (HTTP default 50), `offset` | Same payload shape as `list_verified_borrowers`; MCP default limit is 25 |
| POST `verification-requests/` | JSON `name`, `state`, optional `cusip6`, `category` | Approval-required preview or validation/refusal |
| POST `issuers/{issuer_id}/datasets/` | JSON `datasets`, `options` | Async job reference; no inline compute promise |
| GET `issuers/{issuer_id}/files/` | None | Current file metadata, availability and download locators |
| GET `issuers/{issuer_id}/files/{file_id}/` | None | Attachment bytes after fresh access checks |
| GET `issuers/{issuer_id}/documents/` | Query `kind=all|official_statement|financial_statement`, `limit` 1-200, `offset` >=0 | Document manifest; follow `next_offset` |
| GET `issuers/{issuer_id}/documents/{signed_locator}/` | Use the manifest's exact `download_url` | PDF attachment; current bearer and issuer/index access required |
| GET `jobs/{job_uuid}/` | UUID from returned job reference | Current job state/result |
| GET `datasets/{signed_locator}/` | Use the exact returned `api_download.url` | JSON attachment; also requires the current bearer |

POST bodies are JSON objects with only the listed fields. A dataset request
body is at most 8 MiB (8,388,608 bytes); a larger one gets 413
`request_too_large` with `limit_bytes`. Its options other than the security
selectors take at most 8,192 bytes as JSON (400 otherwise). A verification
request body is at most 64 KiB (400 `invalid_parameters` otherwise).
There is no client-supplied company/user identity. Six routes return application
JSON. The three download routes stream bytes: an uploaded file, debt-document
PDF, or exported dataset JSON. A failed download can return application JSON.

Use HTTP status and each route's payload schema. Inspect application status
where supplied, especially queue, job and approval responses. The uploaded-file
list returns `issuer_id`, `files` and `selection` without a `status` field.
MCP additionally wraps results according to the advertised `outputSchema`;
the six HTTP JSON routes omit that MCP wrapper. Do not assume an MCP
`structuredContent.result` path also exists in direct HTTP.

The HTTP job route can return a paged result: `result.result_paged` is true,
`result.first_page_ref` names its first page, and `result_manifest` is at the
payload root. Reconstruct every page with MCP `fetch`, starting with the `s000`
initializer, before reading artifact fields. These nine banker HTTP routes
have no page-fetch endpoint; an HTTP-only client cannot reconstruct that paged
result through them. Use MCP for reconstruction or request a smaller export.
A paged stub is not the complete artifact.

Example application arguments for `get_debt_details_dataset`:

```json
{
  "issuer_id": "123",
  "datasets": ["outstanding", "groupedRollup", "annualChart", "annualTree"],
  "options": {
    "asOfDate": "2026-09-17",
    "fiscalYearEnd": "06/30",
    "variableRate": 1.0,
    "allBorrowerSecurities": true
  }
}
```

Replace `123` with an authorized returned ID; it is an example, not a granted
borrower. For HTTP, put the issuer ID in the path and omit it from the body.
The [data guide](banker-data-guide.md) defines datasets, fields and transformations.

## Complete result retrieval

1. Submit the dataset request. Check for a queued response and retain `job_ref`.
2. Poll `get_job_status` to `done`, `failed` or `cancelled`. Inspect the terminal
   application outcome; a successful poll alone is not calculation success.
   If the result is paged, reconstruct it before reading its artifact fields.
3. For a done export, GET the returned `api_download.url` with the **current**
   MCP bearer. Verify the file's raw-byte SHA-256 and size against `api_download`.
4. Retain schema, parameters, source dates, warnings and provenance with the data.

The signed locator lasts 24 hours and is bound to owner, company, reference and
content digest. It is not a bearer credential. Another owner cannot use it;
revocation or loss of access refuses it. After expiry, submit a fresh export.
The separate `download_url` is for an authorized MuniPro browser session.
Never send a MuniPro bearer to public EMMA URLs or arbitrary artifact hosts.

JSON files up to 128 MiB are supported; larger exports refuse with 413 and
require narrower datasets/selections. Files are never silently truncated.
Payloads <=1 MiB also appear in retained structured data, potentially through
MCP result pages. Follow every returned page/manifest, including the initializer;
do not confuse JCS-canonical result-page hashes with the JSON file's raw-byte hash.
The existing async retention lifetime and limits apply independently of download
expiry. Large exports remain available through their authenticated file.

Rogo's default connector must be tested for this separate authenticated HTTP
download. An MCP tool result containing a URL does not establish that Rogo can
retrieve its bytes or forward the connector's bearer. If that is unsupported,
use small inline results for the initial demo and agree an authenticated delivery
adapter before claiming complete large-dataset or workbook delivery. Do not put
tokens in prompts, relax authentication, or publish private artifact links.

## Verification approval

The tool creates a pending approval, not a borrower verification. Display its
`approval_url` to the user. The same person signs in to MuniPro and reviews the
stored company-bound request at `/assistant/verification-requests/<uuid>/`.
Opening that page makes no change; its explicit approval form uses the existing
session/CSRF-protected approval rail. The host cannot supply an approval token
or replace stored command arguments. Native approval additionally requires the
workspace's assistant-write admission.

The initial HTTP preview intentionally returns status 400 with
`status: "approval_required"`, `pending_operation_id` and `approval_url`. Parse this application
status before treating every HTTP 400 as invalid input. No coverage request has
been committed at that point; human approval is still required.
The browser shows a submission confirmation after approval. Rogo must preserve
and display the review link even though the preview's application status is 400.
Do not infer that Rogo has consumed the API receipt from the browser confirmation.

Approval rechecks role, profile, scope, entitlement, grant, target and expiry.
A committed result reports `requested` or `already_requested`, a request ID,
and `borrower_verified: false`. Replaying that approval cannot create a second
request. A semantically matching pending staff request is reused. Staff review
and actual verified debt publication remain separate workflows. No request email
or client invitation is sent by this tool.

## Documents and uploaded packages

For debt-document rows, send a MuniPro bearer only if
`download_authentication` is `munipro_oauth_bearer` and the parsed
`download_url` origin is exactly `https://munipro.com`. Never forward the bearer
to EMMA or another origin, including when following redirects. A `public_EMMA`
row is a public source link. Inspect the returned metadata before downloading;
documentation for a newer release does not change an older server's response.

Uploaded files are the current package's Debt Profile/FY Debt Profile workbooks,
DDL, and named attachments when present. Internal Django Upload inputs are
excluded. Missing bytes are reported as unavailable; an ID from an older or
different issuer's package is refused. File locators recheck current package
membership each time.

When an issuer file result carries `ddl.download_page_url`, show it to the banker
as a link they click to download the original `.ddl` file in their MuniPro browser
session. The link is single use and expires at `ddl.download_page_expires_at`, so
ask for a fresh result after it expires. It never needs a token pasted anywhere.

Uploaded-file locators have no promised fixed expiry or creator-owner binding;
each download rechecks the current caller's grant, issuer access and package
membership. The signed 24-hour, owner-bound locator and SHA-256 contract above
applies to generated dataset exports. Uploaded-file metadata reports
`size_bytes` when available but does not promise a raw-byte SHA-256.

Debt documents come from indexed final Official Statements and historical
financial-statement feeds. Each row retains its public EMMA `source_url` and
returns a MuniPro `download_url` with `download_authentication=munipro_oauth_bearer`.
Send the current bearer only to the MuniPro URL. Its signed locator expires
after 24 hours; refresh the manifest on 410. It is not an access credential.
Downloads recheck current issuer/profile/grant and index membership, and use
the existing document cache and aiohttp proxy pipeline. Cache misses require
a configured proxy; direct network fallback is disabled for this route.
The returned PDF is checked against its SHA-256 and byte count before delivery;
the `ETag` contains that SHA-256. A missing canonical document identity returns
503 `document_identity_unavailable`; upstream/cache failures return 502/503
with `Retry-After`. These are explicit failures, not empty or successful PDFs.

Pagination includes historical
matches, not just the latest filing. Inspect `sources`, `coverage`, `matches`
and `requires_obligor_review`. CUSIP-6/issuer-date candidate matches can belong
to a conduit issuer's other obligors. An unavailable feed is a coverage gap,
not evidence that no statements exist. Index completeness is not guaranteed.
When no security has a usable CUSIP, the feeds are not queried and `sources` is
empty; their availability is unknown. This does not exclude those bonds from
debt calculations or issuer-upload retrieval.

## Rogo acceptance record

Run with the intended non-admin user/company and record redacted request IDs,
UTC time, Rogo workspace/client version and results. Verify:

- OAuth consent and a fully paged catalog confined to the banker profile.
- Borrower list, a visible issuer and a foreign/private issuer refusal.
- Dataset job, complete authenticated JSON download and matching SHA-256.
- Workbook/DDL download, file not found and unauthorized-issuer refusal;
- Indexed PDF download with bearer, matching ETag/hash, and cookie-only refusal.
  separately check different-owner refusal for generated dataset exports.
- All document-manifest pages and explicit source-gap handling.
- Verification preview, human approval, committed receipt and harmless replay.
- Refresh, disconnect, revoked-token refusal and profile-change refusal.

Server-side tests and published schemas do not establish actual Rogo acceptance.
The native presentation skill is described in the
[banker presentation guide](banker-presentation-guide.md).
