Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/sdk-examples-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ jobs:
applets:
- 'sdk/typescript/applets/**'
- '.github/workflows/sdk-examples-ci.yml'
search-documents-ui:
- 'agents/react/search-documents-ui/**'
- '.github/workflows/sdk-examples-ci.yml'
search-documents-mcp:
- 'agents/typescript/search-documents-mcp/**'
- '.github/workflows/sdk-examples-ci.yml'

dotnet:
name: C# format & build
Expand Down Expand Up @@ -87,6 +93,10 @@ jobs:
path: agents/react/next-agent-chat
- project: applets
path: sdk/typescript/applets
- project: search-documents-ui
path: agents/react/search-documents-ui
- project: search-documents-mcp
path: agents/typescript/search-documents-mcp

defaults:
run:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,5 @@ the SDK and as a validation tool when upgrading to a new SDK version.
| Example | Stack | Description |
|--------------------------------------------------------------------------------------|----------------------------|----------------------------------------------------------------------------------------------|
| [agents/react/next-agent-chat/](agents/react/next-agent-chat/) | TypeScript, Next.js, React | AI agent chat interface with A2A streaming, AI SDK integration, and real-time messaging |
| [agents/react/search-documents-ui/](agents/react/search-documents-ui/) | TypeScript, React, Express | React/Express chat UI provisioning a Corti agent wired to a scoped document-search MCP |
| [agents/typescript/search-documents-mcp/](agents/typescript/search-documents-mcp/) | TypeScript, MCP, Express | MCP server exposing a RAG document-search tool with per-patient access control |
29 changes: 29 additions & 0 deletions agents/react/search-documents-ui/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ── Corti API credentials (OAuth2 client-credentials) — from the Corti Console ──
# Read server-side only; never prefix with VITE_ (Vite inlines VITE_* vars into
# the browser bundle, which would ship these secrets to every visitor).
CORTI_ENVIRONMENT=us # us | eu
CORTI_TENANT_NAME=your-tenant
CORTI_CLIENT_ID=your-client-id
CORTI_CLIENT_SECRET=your-client-secret

# ── REQUIRED — the Search Documents MCP the orchestrator is wired to ───────────
# MCP_URL must be reachable by Corti (a public/tunnelled HTTPS URL).
# The server will NOT start if MCP_URL or MCP_NAME is missing.
MCP_URL=https://your-mcp-host.example.com/mcp
MCP_NAME=Search Docs Svc

# ── REQUIRED: shared secret for signing scope tokens ──────────────────────────
# Must match search-documents-mcp's MCP_SCOPE_SECRET. The server refuses to start
# if unset (no default). Generate one with: openssl rand -hex 32
MCP_SCOPE_SECRET=

# ── REQUIRED — the orchestrator's system prompt ───────────────────────────────
# Multiline value: keep it double-quoted, and use single quotes for any quoted
# text inside (embedded double quotes would end the value early).
SYSTEM_PROMPT="You answer questions using the search_documents tool. For every question, call search_documents and answer from its results.

Use only the information search_documents returns. Never use prior or outside knowledge, and never add your own advice, recommendations, or assumptions. If it returns nothing relevant, reply: 'I don't have that information in the available documents.'

Each returned passage is labelled with its patient (e.g. 'patient John B. Placeholder (000-MOCK-5678)') or 'reference'. If the passages relevant to the question belong to more than one patient and the question doesn't say which, ask the user which patient before answering, listing the patients found — do not answer for just one of them. If they all belong to one patient, answer directly and name that patient.

Answer concisely. State each fact once and do not restate or summarize what you already said. Cite the source inline."
126 changes: 126 additions & 0 deletions agents/react/search-documents-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# search-documents-ui

A React + Express app that connects to Corti, **provisions an orchestrator agent
wired to a document-search MCP on first run**, lets a clinician sign in, and
chats with the agent — minting scoped access tokens so the agent's MCP retrieval
only returns records that clinician is allowed to see.

It's the UI half of a shareable demo; the retrieval half is
[search-documents-mcp](../../typescript/search-documents-mcp).

## High-level flow

```
Connect to Corti → Agent setup → Clinician sign-in → Patient panel → Chat
(detect by MCP URL; (mock picker) (+ Start chat) (scoped to the
create if missing) clinician's panel)
```

- **Agent setup** detects an existing orchestrator by its **MCP URL** (any
agent name). If none exists, it shows a confirmation screen (the system prompt
+ MCP config the agent will get) with a field for your preferred agent name,
then creates it in your tenant.
- Each chat message carries a short-lived signed **scope token** (the clinician's
patient panel). Corti forwards it to the MCP, which verifies it and filters
retrieval. No agent-list page and no ID/config side panels — just the chat box.

## Setup

```bash
npm install
cp .env.example .env # then fill it in (see below)
```

## Environment variables

| Variable | Required | Purpose |
|----------|----------|---------|
| `CORTI_ENVIRONMENT` | no | Corti environment (`us` or `eu`). Defaults to `us`. |
| `CORTI_TENANT_NAME` | yes | Your Corti tenant name. |
| `CORTI_CLIENT_ID` | yes | OAuth client id for the Corti API. |
| `CORTI_CLIENT_SECRET` | yes | OAuth client secret for the Corti API. Server-side only — no `VITE_` prefix, so Vite never bundles it into the browser. |
| `MCP_URL` | **yes** | Public HTTPS URL of the Search Documents MCP (e.g. `https://<tunnel>/mcp`). Must be reachable by Corti. **The server won't start without it.** |
| `MCP_NAME` | **yes** | Name the MCP server is registered under on the agent; the scope-token DataPart's `mcp_name` must match it. **The server won't start without it.** |
| `MCP_SCOPE_SECRET` | **yes** | HMAC secret used to sign scope tokens. Must match the same variable in search-documents-mcp. No default: the server refuses to start if unset, so tokens can't be signed with a guessable key. Generate one with `openssl rand -hex 32`. |
| `SYSTEM_PROMPT` | **yes** | The orchestrator's system prompt. No default; the server won't start without it. |

`MCP_URL`, `MCP_NAME`, `SYSTEM_PROMPT`, and `MCP_SCOPE_SECRET` are required with
no fallback; the server fails fast with a clear message if any is missing.

## Running

The MCP must be running and publicly reachable first, since `MCP_URL` is required
at startup:

```bash
# 1. Start the MCP and expose it (in ../../typescript/search-documents-mcp)
npm install && npm run build && npm run reindex && npm run start:http
ngrok http 3000 # set this app's MCP_URL to the tunnel URL + /mcp

# 2. Start this app
npm start # Express backend (:3003) + Vite frontend (:5175)
```

Then open http://localhost:5175 → **Connect → Agent setup** (name + create) **→
pick a clinician → Start chat**. You can also run the halves separately:

```bash
npm run server # backend only, :3003 (tsx watch)
npm run dev # frontend only, :5175 (proxies /api to :3003)
```

> Each user runs against their own Corti tenant; the setup step provisions the
> orchestrator *in their tenant*. The MCP URL and secret are owner config (env),
> so a person you share with mainly supplies their own Corti credentials.

> **Uploads:** the document-upload page calls the MCP's `/ingest`, which is
> disabled by default. To use it, start the MCP with `ALLOW_INGEST=true` (e.g.
> `ALLOW_INGEST=true npm run start:http`). Leave it off if you don't need uploads.

## Hardcoded / mock data

There's no real identity provider — [directory.ts](server/directory.ts) holds two mock
directories: `PATIENTS` (mock MRNs → display names) and `CLINICIANS` (each with a
`patients` panel of MRNs they may see). The MRNs match the patient records in
search-documents-mcp's docs so scoped retrieval lines up, and the signed-in
clinician's panel becomes the scopes in the minted token.

## How the token flow works

On each message the backend mints an HMAC-signed JWT-style token containing the
signed-in clinician's patient scopes (and display names) and attaches it as a
bearer-auth DataPart. Corti forwards it to the MCP, which verifies the signature
with the shared `MCP_SCOPE_SECRET`, records the scopes against the conversation's
`contextId`, and filters retrieval. Tokens are short-lived (5 minutes). The
signing logic lives in [token.ts](server/token.ts) and mirrors the MCP's verifier;
[mcp.ts](server/mcp.ts) mints the token using the audience the MCP expects
(`MCP_AUDIENCE = 'search-documents-mcp'`, defined in [config.ts](server/config.ts)).

## Layout

```
server/ Express backend (TypeScript, run with tsx)
index.ts Express app wiring; mounts the routers under /api
config.ts env loading + fail-fast validation; MCP audience constant
corti.ts Corti SDK connection + shared error/guard helpers
session.ts in-process signed-in clinician + active MCP name
directory.ts mock PATIENTS / CLINICIANS (stubbed identity)
mcp.ts scope-token minting from the clinician's panel
token.ts scope-token signing (HMAC); the MCP holds the verifier
routes/
auth.ts connect to Corti
clinicians.ts list clinicians / sign in
agent.ts detect-by-URL / provision the orchestrator
chat.ts start chat (warm-up + pre-bind) + message relay
documents.ts upload a doc to the MCP, scope-authorized server-side
src/
App.jsx top-level flow/state
AuthView.jsx connect to Corti
AgentSetupView.jsx detect-by-URL / confirm + create the orchestrator
ClinicianSignInView.jsx mock clinician picker
PatientPanelView.jsx the clinician's patients + Start chat / Upload
UploadView.jsx upload a doc, scoped to a patient or shared
AgentChatView.jsx the chat box (no side panels)
api.js frontend API client
ui.jsx shared UI primitives (Banner, ScreenHeader, …)
```
52 changes: 52 additions & 0 deletions agents/react/search-documents-ui/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!!dist", "!!node_modules", "!!*.config.mjs", "!!*.config.js"]
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended",
"style": {
"useBlockStatements": "error",
"useConst": "error",
"noParameterAssign": "warn",
"useNodejsImportProtocol": "warn"
},
"correctness": {
"noUnusedVariables": "warn",
"useExhaustiveDependencies": "off",
"useHookAtTopLevel": "off"
},
"suspicious": {
"noExplicitAny": "warn",
"noConsole": "off"
}
}
}
}
15 changes: 15 additions & 0 deletions agents/react/search-documents-ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RAG UI · Corti</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=Inter:wght@400;600;700;800;900&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Loading
Loading