@aprovan/mcp-app-server 0.1.0-dev.4d82df8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +23 -0
- package/E2E_TESTING.md +224 -0
- package/LICENSE +373 -0
- package/README.md +164 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.js +990 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/dist/runtime/assets/index-tRNuU9ap.js +1059 -0
- package/dist/runtime/index.html +38 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +1511 -0
- package/dist/server.js.map +1 -0
- package/dist/shell/shell.js +67 -0
- package/docs/widget-preview.png +0 -0
- package/e2e/__snapshots__/.gitkeep +0 -0
- package/e2e/global-setup.ts +114 -0
- package/e2e/global-teardown.ts +15 -0
- package/e2e/visual-regression.test.ts +86 -0
- package/e2e/widget-smoke.test.ts +109 -0
- package/index.html +32 -0
- package/package.json +51 -0
- package/playwright.config.ts +43 -0
- package/src/__tests__/live-update.test.ts +158 -0
- package/src/__tests__/local-backend.test.ts +100 -0
- package/src/__tests__/memory-backend.ts +144 -0
- package/src/__tests__/registry-backend.test.ts +256 -0
- package/src/__tests__/services.test.ts +153 -0
- package/src/__tests__/shim.test.ts +60 -0
- package/src/__tests__/widget-store.test.ts +188 -0
- package/src/e2e-visual.ts +148 -0
- package/src/index.ts +608 -0
- package/src/live-update.ts +150 -0
- package/src/logger.ts +44 -0
- package/src/reference-widgets/live-dashboard.ts +162 -0
- package/src/registry-backend.ts +306 -0
- package/src/runtime/index.html +38 -0
- package/src/runtime/main.ts +164 -0
- package/src/scripts/export-artifacts.ts +51 -0
- package/src/server.ts +398 -0
- package/src/services.ts +307 -0
- package/src/shell/main.ts +172 -0
- package/src/shim.ts +106 -0
- package/src/tunnel.ts +123 -0
- package/src/widget-store/index.ts +10 -0
- package/src/widget-store/local-backend.ts +77 -0
- package/src/widget-store/store.ts +242 -0
- package/src/widget-store/types.ts +53 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +14 -0
- package/vite.runtime.config.ts +28 -0
- package/vite.shell.config.ts +30 -0
- package/vitest.config.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# @aprovan/mcp-app-server
|
|
2
|
+
|
|
3
|
+
MCP App Server — hosts MCP tools that save and render interactive widgets in Claude Desktop and Claude web, optionally bridged to third-party APIs via the Aprovan Registry.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Widgets are stored as **raw, uncompiled** `.tsx`/`.ts` source files and compiled **in the
|
|
8
|
+
browser** at render time using the shared `@aprovan/patchwork-compiler` runtime — the same
|
|
9
|
+
path the chat app uses. The server does no widget compilation, Tailwind injection, or CDN
|
|
10
|
+
resolution; those concerns belong to the compiler + image packages.
|
|
11
|
+
|
|
12
|
+
The MCP Apps protocol imposes two constraints that shape the render path:
|
|
13
|
+
|
|
14
|
+
1. The resource document Claude renders **must itself be the app** that connects to the host
|
|
15
|
+
(`App.connect()` → `window.parent`), and
|
|
16
|
+
2. it runs under a **strict CSP with no `unsafe-eval`**, so esbuild-wasm cannot run there.
|
|
17
|
+
|
|
18
|
+
So rendering is split across two pieces (the "bridged" design):
|
|
19
|
+
|
|
20
|
+
- **Shell** (`/shell/shell.js`) — a small bundle of the ext-apps client that is the resource
|
|
21
|
+
document. It connects to Claude (handshake + sizing), and embeds the runtime in a nested,
|
|
22
|
+
**CSP-free** iframe served from the widget host. It relays the widget's
|
|
23
|
+
service / live-update / `updateContext` calls to the host over the ext-apps `App`.
|
|
24
|
+
- **Runtime** (`/runtime/`) — runs inside that nested iframe (no host CSP), fetches the saved
|
|
25
|
+
widget's raw source from `/widget/:name/:hash/files`, compiles it with esbuild-wasm, loads
|
|
26
|
+
the image (Tailwind/React) from the CDN, and mounts it. The widget's `window.patchwork.*` and
|
|
27
|
+
service-namespace calls are forwarded to the shell via `postMessage`.
|
|
28
|
+
|
|
29
|
+
Tools:
|
|
30
|
+
|
|
31
|
+
- `save_widget` persists a widget's raw files (+ manifest) to the widget store and returns the
|
|
32
|
+
shell resource pointed at it (with startup `inputs`).
|
|
33
|
+
- `render_widget` re-renders any previously saved widget by name/hash.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm dev # start with tsx watch (hot-reload)
|
|
39
|
+
# or
|
|
40
|
+
pnpm build && pnpm start
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Server runs on `http://0.0.0.0:3000` by default.
|
|
44
|
+
|
|
45
|
+
## Registry integration (APR-61)
|
|
46
|
+
|
|
47
|
+
The server can optionally connect to the [Aprovan Registry](https://github.com/AprovanLabs/registry) MCP server to expose 30+ third-party APIs (GitHub, Slack, Stripe, Datadog, …) as callable widget services.
|
|
48
|
+
|
|
49
|
+
### Enabling the Registry backend
|
|
50
|
+
|
|
51
|
+
Set `REGISTRY_PROVIDERS` before starting the server:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
REGISTRY_PROVIDERS=github,slack,stripe pnpm dev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The server will spawn `npx @utdk/mcp` automatically with the requested providers.
|
|
58
|
+
|
|
59
|
+
### Credential configuration
|
|
60
|
+
|
|
61
|
+
The server forwards all environment variables to the Registry child process, so you only need to set credentials once in the parent environment. Below are the most common providers:
|
|
62
|
+
|
|
63
|
+
| Provider | Required env vars | Notes |
|
|
64
|
+
|----------|-------------------|-------|
|
|
65
|
+
| **GitHub** | `GITHUB_TOKEN` | Personal access token or fine-grained token |
|
|
66
|
+
| **Stripe** | `STRIPE_SECRET_KEY` | Secret key (`sk_test_…` or `sk_live_…`) |
|
|
67
|
+
| **Slack** | `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET` | OAuth2 client credentials; flows via client-credentials grant |
|
|
68
|
+
| **Notion** | `NOTION_API_KEY` | Integration token from Notion developer portal |
|
|
69
|
+
| **Jira** | `JIRA_EMAIL`, `JIRA_API_TOKEN` | User email + API token (basic auth) |
|
|
70
|
+
| **Datadog** | `DD_API_KEY`, `DD_APP_KEY` | API key + application key |
|
|
71
|
+
| **HubSpot** | `HUBSPOT_CLIENT_ID`, `HUBSPOT_CLIENT_SECRET` | OAuth2 client credentials |
|
|
72
|
+
| **Linear** | `LINEAR_API_KEY` | Personal API key |
|
|
73
|
+
| **Airtable** | `AIRTABLE_API_KEY` | Personal access token |
|
|
74
|
+
| **Zendesk** | `ZENDESK_CLIENT_ID`, `ZENDESK_CLIENT_SECRET` | OAuth2 client credentials |
|
|
75
|
+
| **Salesforce** | `SALESFORCE_CLIENT_ID`, `SALESFORCE_CLIENT_SECRET` | Connected app credentials |
|
|
76
|
+
| **SendGrid** | `SENDGRID_API_KEY` | API key |
|
|
77
|
+
| **Twilio** | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` | Account SID + auth token |
|
|
78
|
+
| **Intercom** | `INTERCOM_ACCESS_TOKEN` | Access token from Intercom developer hub |
|
|
79
|
+
| **Figma** | `FIGMA_ACCESS_TOKEN` | Personal access token |
|
|
80
|
+
| **OpenAI** | `OPENAI_API_KEY` | API key |
|
|
81
|
+
| **Discord** | `DISCORD_BOT_TOKEN` | Bot token |
|
|
82
|
+
|
|
83
|
+
For providers not listed here, inspect the provider package at `packages/utdk/<provider>/package.json` in the registry repo and look at the `utdk.auth` field.
|
|
84
|
+
|
|
85
|
+
### Local development example
|
|
86
|
+
|
|
87
|
+
Create a `.env` file (never commit this):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# .env — loaded by dotenv or direnv
|
|
91
|
+
REGISTRY_PROVIDERS=github,stripe
|
|
92
|
+
|
|
93
|
+
GITHUB_TOKEN=ghp_...
|
|
94
|
+
STRIPE_SECRET_KEY=sk_test_...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then start:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# with dotenv-cli
|
|
101
|
+
dotenv pnpm dev
|
|
102
|
+
|
|
103
|
+
# or export manually
|
|
104
|
+
export $(grep -v '^#' .env | xargs) && pnpm dev
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Using Registry tools in a widget
|
|
108
|
+
|
|
109
|
+
Once the Registry is connected, any tool namespace (e.g. `github`, `stripe`) can be requested in the `save_widget` `services` parameter:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Tool: save_widget
|
|
113
|
+
{
|
|
114
|
+
"source": "...",
|
|
115
|
+
"services": ["github", "stripe"]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Inside the widget, services are available as global namespace objects:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// List repositories
|
|
123
|
+
const repos = await github.repos_list({ per_page: 10 });
|
|
124
|
+
|
|
125
|
+
// Create a payment intent
|
|
126
|
+
const intent = await stripe.payment_intents_create({
|
|
127
|
+
amount: 2000,
|
|
128
|
+
currency: "usd",
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The call chain is: widget → `callServerTool` → `ServiceBridge` → `RegistryBackend` → `@utdk/mcp` → provider API.
|
|
133
|
+
|
|
134
|
+
### Overriding the Registry command
|
|
135
|
+
|
|
136
|
+
By default the server uses `npx @utdk/mcp`. You can point to a locally built binary instead:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
REGISTRY_COMMAND=/path/to/registry/apps/mcp-server/dist/server.js \
|
|
140
|
+
REGISTRY_PROVIDERS=github \
|
|
141
|
+
GITHUB_TOKEN=ghp_... \
|
|
142
|
+
node dist/server.js
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Namespace collision avoidance
|
|
146
|
+
|
|
147
|
+
Registry tool namespaces (one per provider) are registered separately from any locally-defined service tools. To check which namespaces are active, call `search_services` with no arguments from a Claude conversation using the connected MCP server.
|
|
148
|
+
|
|
149
|
+
## Environment variables
|
|
150
|
+
|
|
151
|
+
| Variable | Default | Description |
|
|
152
|
+
|----------|---------|-------------|
|
|
153
|
+
| `PORT` | `3000` | HTTP port to listen on |
|
|
154
|
+
| `HOST` | `0.0.0.0` | Host interface |
|
|
155
|
+
| `REGISTRY_PROVIDERS` | _(unset)_ | Enable Registry backend; comma-separated provider list |
|
|
156
|
+
| `REGISTRY_COMMAND` | `npx` | Command used to spawn the Registry MCP server |
|
|
157
|
+
| `REGISTRY_ARGS` | _(unset)_ | Space-separated extra args appended after `@utdk/mcp` |
|
|
158
|
+
|
|
159
|
+
## MCP endpoints
|
|
160
|
+
|
|
161
|
+
| Method | Path | Description |
|
|
162
|
+
|--------|------|-------------|
|
|
163
|
+
| `POST` | `/mcp` | MCP Streamable HTTP endpoint |
|
|
164
|
+
| `GET` | `/health` | Health check |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { VirtualFile, Manifest } from '@aprovan/patchwork-compiler';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single buffered event for a data stream.
|
|
6
|
+
*/
|
|
7
|
+
interface StreamEvent {
|
|
8
|
+
seq: number;
|
|
9
|
+
data: unknown;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Push a data update to all sessions that have subscribed to `stream`.
|
|
14
|
+
*
|
|
15
|
+
* The event is appended to the ring buffer. Each subscribed session's McpServer
|
|
16
|
+
* then sends a `notifications/tools/list_changed` notification to the host.
|
|
17
|
+
* The host forwards this to the widget, which uses it as a signal to call
|
|
18
|
+
* `poll_updates` and retrieve the buffered data.
|
|
19
|
+
*
|
|
20
|
+
* @returns The new sequence number assigned to this event.
|
|
21
|
+
*/
|
|
22
|
+
declare function pushStreamUpdate(stream: string, data: unknown): Promise<number>;
|
|
23
|
+
|
|
24
|
+
interface ServiceBackend {
|
|
25
|
+
call(namespace: string, procedure: string, args: unknown[]): Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
interface ServiceToolInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
namespace: string;
|
|
30
|
+
procedure: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
parameters?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
interface ServiceBridgeConfig {
|
|
35
|
+
backend: ServiceBackend;
|
|
36
|
+
tools: ServiceToolInfo[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface FileStats {
|
|
40
|
+
size: number;
|
|
41
|
+
mtime: Date;
|
|
42
|
+
isFile(): boolean;
|
|
43
|
+
isDirectory(): boolean;
|
|
44
|
+
}
|
|
45
|
+
interface DirEntry {
|
|
46
|
+
name: string;
|
|
47
|
+
isFile(): boolean;
|
|
48
|
+
isDirectory(): boolean;
|
|
49
|
+
}
|
|
50
|
+
interface FSProvider {
|
|
51
|
+
readFile(path: string, encoding?: "utf8" | "base64"): Promise<string>;
|
|
52
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
53
|
+
unlink(path: string): Promise<void>;
|
|
54
|
+
stat(path: string): Promise<FileStats>;
|
|
55
|
+
mkdir(path: string, options?: {
|
|
56
|
+
recursive?: boolean;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
readdir(path: string): Promise<DirEntry[]>;
|
|
59
|
+
rmdir(path: string, options?: {
|
|
60
|
+
recursive?: boolean;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
exists(path: string): Promise<boolean>;
|
|
63
|
+
}
|
|
64
|
+
interface StoredWidget {
|
|
65
|
+
/** Storage path of the widget directory (e.g. "widgets/timer/abc123"). */
|
|
66
|
+
path: string;
|
|
67
|
+
resourceUri: string;
|
|
68
|
+
/** Raw, uncompiled widget source files. Compilation happens in the browser. */
|
|
69
|
+
files: VirtualFile[];
|
|
70
|
+
/** Entry file path within {@link files} (e.g. "main.tsx"). */
|
|
71
|
+
entry: string;
|
|
72
|
+
manifest: Manifest;
|
|
73
|
+
createdAt: number;
|
|
74
|
+
}
|
|
75
|
+
interface StoredWidgetInfo {
|
|
76
|
+
path: string;
|
|
77
|
+
resourceUri: string;
|
|
78
|
+
name: string;
|
|
79
|
+
version: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
services?: string[];
|
|
82
|
+
entry?: string;
|
|
83
|
+
createdAt: number;
|
|
84
|
+
}
|
|
85
|
+
interface WidgetStoreOptions {
|
|
86
|
+
storageDir?: string;
|
|
87
|
+
backend?: FSProvider;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Persistent store for **raw, uncompiled** widget source files.
|
|
92
|
+
*
|
|
93
|
+
* Widgets are saved as their original `.tsx`/`.ts` files plus a `manifest.json`;
|
|
94
|
+
* compilation happens in the browser via the shared `@aprovan/patchwork-compiler`
|
|
95
|
+
* runtime when the widget is rendered. Layout:
|
|
96
|
+
*
|
|
97
|
+
* ```
|
|
98
|
+
* widgets/<name>/<hash>/
|
|
99
|
+
* files/main.tsx
|
|
100
|
+
* files/price-card.tsx
|
|
101
|
+
* manifest.json { ...manifest, entry, createdAt }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare class WidgetStore {
|
|
105
|
+
private provider;
|
|
106
|
+
private storageDir;
|
|
107
|
+
constructor(options?: WidgetStoreOptions);
|
|
108
|
+
private fullPath;
|
|
109
|
+
private readFilesRecursive;
|
|
110
|
+
saveWidget(hash: string, files: VirtualFile[], manifest: Manifest, entry: string): Promise<StoredWidget>;
|
|
111
|
+
getWidget(name: string, hash: string): Promise<StoredWidget | null>;
|
|
112
|
+
listWidgets(): Promise<StoredWidgetInfo[]>;
|
|
113
|
+
deleteWidget(name: string, hash: string): Promise<boolean>;
|
|
114
|
+
hasWidget(name: string, hash: string): Promise<boolean>;
|
|
115
|
+
resourceUriFor(name: string, hash: string): string;
|
|
116
|
+
loadAll(): Promise<StoredWidget[]>;
|
|
117
|
+
}
|
|
118
|
+
declare function getWidgetStore(options?: WidgetStoreOptions): WidgetStore;
|
|
119
|
+
declare function resetWidgetStore(): void;
|
|
120
|
+
|
|
121
|
+
interface McpAppServerOptions {
|
|
122
|
+
services?: ServiceBridgeConfig;
|
|
123
|
+
/** Base URL for serving widgets (e.g., tunnel URL). Defaults to localhost:3002. */
|
|
124
|
+
widgetBaseUrl?: string;
|
|
125
|
+
}
|
|
126
|
+
declare function createMcpAppServer(options?: McpAppServerOptions): McpServer;
|
|
127
|
+
|
|
128
|
+
export { type McpAppServerOptions, type ServiceBackend, type ServiceBridgeConfig, type ServiceToolInfo, type StoredWidget, type StoredWidgetInfo, type StreamEvent, WidgetStore, type WidgetStoreOptions, createMcpAppServer, getWidgetStore, pushStreamUpdate, resetWidgetStore };
|