1.add initial spike for mcp ext app

This commit is contained in:
Kent Wang
2026-03-12 17:08:20 +08:00
parent 7f33ae9dfc
commit d327ea40ed
2 changed files with 308 additions and 24 deletions

222
CLAUDE.md Normal file
View File

@@ -0,0 +1,222 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## About Insomnia
Insomnia is an open-source, cross-platform API client for GraphQL, REST, WebSockets, SSE, gRPC and other HTTP protocols. It's built on Electron with a React frontend.
## Development Commands
**Prerequisites:**
- Node.js version: `22.20.0` (see `.nvmrc`)
- npm >= 10
**Setup:**
```bash
npm i
```
**Development:**
```bash
npm run dev # Start app with live reload
npm run dev:autoRestart # Start with both renderer live reload and main process auto-restart
```
**Code Quality:**
```bash
npm run lint # Run linting across all workspaces
npm run type-check # Run TypeScript type checking across all workspaces
npm test # Run tests across all workspaces
```
**Building:**
```bash
npm run app-build # Build the app
npm run app-package # Package the app for distribution
```
**Inso CLI Development:**
```bash
npm run inso-start # Start Inso CLI in watch mode
npm run inso-package # Build and package Inso CLI
./packages/insomnia-inso/bin/inso -v # Run Inso CLI locally
```
**Testing:**
```bash
# Smoke tests
npm run test:smoke:dev # Run smoke tests in dev mode
npm run test:smoke:build # Run smoke tests on built app
npm run test:smoke:package # Run smoke tests on packaged app
# Critical tests
npm run test:crit:dev # Run critical tests in dev mode
npm run test:crit:package # Run critical tests on packaged app
```
**Package-specific commands:**
```bash
npm run <script> -w insomnia # Run script in main insomnia package
npm run <script> -w insomnia-inso # Run script in inso CLI package
npm run <script> -w insomnia-smoke-test # Run script in smoke test package
```
## Repository Structure
This is a monorepo using npm workspaces:
- **`packages/insomnia`**: Main Electron application
- **`packages/insomnia-inso`**: CLI tool that reuses logic from the main app
- **`packages/insomnia-testing`**: Testing utilities
- **`packages/insomnia-api`**: API client for Insomnia backend services
- **`packages/insomnia-smoke-test`**: End-to-end Playwright tests
- **`packages/insomnia-scripting-environment`**: Scripting environment for request execution
## Main Package Architecture (`packages/insomnia`)
### Key Directories
- **`entry.main.ts`**: Entry point for the Electron application
- **`src/main/`**: Main process code (window management, updates, analytics, IPC handlers)
- **`src/ui/`**: React components, routes, hooks, and Tailwind styling
- **`src/common/`**: Shared code between main and renderer processes (database, constants)
- **`src/models/`**: Data models and database schema (requests, workspaces, environments, etc.)
- **`src/sync/`**: Synchronization logic (Git Sync, Cloud Sync, Local Vault)
- **`src/network/`**: HTTP client logic and authentication (OAuth, etc.)
- **`src/templating/`**: Nunjucks template rendering
- **`src/plugins/`**: Plugin installation and usage
### Data Models
All data models are in `src/models/`. Each file defines:
- Type definitions
- Database schema
- CRUD operations for that model type
The `src/models/index.ts` consolidates all models and provides utility functions.
### State Management & Routing
- **No Redux**: The app has migrated away from Redux to React Router
- **React Router**: Uses file-based routing in `src/routes/`
- Dynamic segments: `organization.$organizationId.project.$projectId._index.tsx`
- `loader`/`clientLoader`: Fetch data from database
- `action`/`clientAction`: Handle mutations
- **Database**: NeDB (in-memory/file-based) accessed via `src/common/database.ts`
## Synchronization Architecture
Three storage backends are supported:
### 1. Local Vault
- Data stored locally in NeDB database
- No cloud sync
### 2. Git Sync
- Implementation: `src/sync/git/`
- Uses `isomorphic-git` library
- Core logic: `git-vcs.ts` (clone, push, pull, commit operations)
- Virtual filesystem in Electron
### 3. Cloud Sync
- Implementation: `src/sync/vcs/vcs.ts`
- Syncs to Insomnia's cloud backend via API
- Handles merging and conflict resolution
- Supports end-to-end encryption
Common types: `src/sync/types.ts` (Snapshot, Branch, MergeConflict, etc.)
## Technical Stack
- **Framework**: Electron 38.4.0 + React 18
- **UI**: React components with Tailwind CSS 4 and react-aria
- **Routing**: React Router 7 (file-based routing)
- **HTTP Client**: libcurl via `@getinsomnia/node-libcurl` (for deep HTTP control)
- **Database**: NeDB (`@seald-io/nedb`)
- **Code Editor**: CodeMirror 5 and Monaco Editor
- **Testing**: Vitest (unit tests) + Playwright (e2e tests)
- **Build**: esbuild + Electron Builder
- **Styling**: Tailwind CSS 4 with react-aria components
## Testing
### Unit Tests
- Located alongside the file under test: `src/common/__tests__/database.test.js`
- Uses Vitest
- Run with `npm test`
### Smoke Tests
- Located in `packages/insomnia-smoke-test`
- Uses Playwright
- Tests both built and packaged versions
## IPC Communication
Main process and renderer process communicate via Electron IPC:
- Handlers registered in `src/main/ipc/`
- Used for accessing Node.js APIs from renderer
## Important Technical Notes
### Known Technical Debt
- NeDB is unmaintained but deeply integrated (migration planned)
- CodeMirror 5 is unmaintained
- Sync code needs refactoring
- Template rendering needs cleanup
- `apiconnect-wsdl` has restrictive engine config - after `npm install`, the package-lock is manually edited
### Native Modules
- `@getinsomnia/node-libcurl` requires special handling
- Must be rebuilt for Electron: `npm run install-libcurl-electron`
- Platform-specific builds can be challenging (Windows, Mac, Linux)
### Inso CLI Architecture
The CLI (`insomnia-inso`) reuses logic from the main app:
1. Imports from `insomnia` and `insomnia-testing` packages
2. Transpiled to CommonJS with esbuild
3. Packaged as executable with `pkg`
4. Note: Currently bundles almost entire renderer (includes React components despite being Node-only)
## Working with Data Models
When modifying data models:
1. Update the model file in `src/models/`
2. Update TypeScript types
3. Consider migration logic if schema changes
4. Update sync logic if the model is synced
## Electron Upgrades
When upgrading Electron:
1. Update `.npmrc`
2. Update `.nvmrc`
3. Update `packages/insomnia/package.json` (electron and node-libcurl versions)
4. Update `shell.nix`
5. Test native module compatibility
## Branch Strategy
- Main development branch: `develop`
- Create PRs against `develop`

View File

@@ -1,3 +1,4 @@
import { getToolUiResourceUri, McpUiToolMetaSchema } from '@modelcontextprotocol/ext-apps/app-bridge';
import { type RJSFSchema } from '@rjsf/utils';
import type { EditorChange } from 'codemirror';
import React, { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -7,10 +8,11 @@ import { useLatest } from 'react-use';
import { docsMcpClient } from '~/common/documentation';
import { buildResourceJsonSchema, fillUriTemplate } from '~/common/mcp-utils';
import type { McpReadyState } from '~/main/mcp/types';
import type { McpAppResourceData, McpReadyState } from '~/main/mcp/types';
import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId';
import { Link } from '~/ui/components/base/link';
import { EnvironmentKVEditor } from '~/ui/components/editors/environment-key-value-editor/key-value-editor';
import { McpAppEmbeddedView } from '~/ui/components/mcp/mcp-app-embedded-view';
import { InsomniaRjsfForm, type InsomniaRjsfFormHandle } from '~/ui/components/rjsf';
import { type AuthTypes } from '../../../common/constants';
@@ -67,6 +69,7 @@ export const McpRequestPane: FC<Props> = ({
const primitiveId = `${selectedPrimitiveItem?.type}_${selectedPrimitiveItem?.name}`;
const { activeRequest, activeRequestMeta, requestPayload } = useRequestLoaderData()! as McpRequestLoaderData;
const latestRequestPayloadRef = useLatest(requestPayload);
const [appResourceData, setAppResourceData] = useState<McpAppResourceData | null>(null);
const { activeProject } = useWorkspaceLoaderData()!;
@@ -136,28 +139,31 @@ export const McpRequestPane: FC<Props> = ({
// validate the form before sending, but don't block sending on validation errors for debug purpose
rjsfFormRef.current?.validate();
try {
if (selectedPrimitiveItem?.type === 'tools') {
await window.main.mcp.primitive.callTool({
name: selectedPrimitiveItem?.name || '',
arguments: mcpParams[primitiveId],
requestId: requestId,
});
} else if (selectedPrimitiveItem?.type === 'resources') {
await window.main.mcp.primitive.readResource({
requestId,
uri: selectedPrimitiveItem?.uri || '',
});
} else if (selectedPrimitiveItem?.type === 'resourceTemplates') {
await window.main.mcp.primitive.readResource({
requestId,
uri: fillUriTemplate(selectedPrimitiveItem.uriTemplate, mcpParams[primitiveId] || {}),
});
} else if (selectedPrimitiveItem?.type === 'prompts') {
await window.main.mcp.primitive.getPrompt({
requestId,
name: selectedPrimitiveItem?.name || '',
arguments: mcpParams[primitiveId],
});
if (selectedPrimitiveItem) {
const { type: primitiveType } = selectedPrimitiveItem;
if (primitiveType === 'tools') {
await window.main.mcp.primitive.callTool({
name: selectedPrimitiveItem.name || '',
arguments: mcpParams[primitiveId],
requestId: requestId,
});
} else if (primitiveType === 'resources') {
await window.main.mcp.primitive.readResource({
requestId,
uri: selectedPrimitiveItem.uri || '',
});
} else if (primitiveType === 'resourceTemplates') {
await window.main.mcp.primitive.readResource({
requestId,
uri: fillUriTemplate(selectedPrimitiveItem.uriTemplate, mcpParams[primitiveId] || {}),
});
} else if (primitiveType === 'prompts') {
await window.main.mcp.primitive.getPrompt({
requestId,
name: selectedPrimitiveItem?.name || '',
arguments: mcpParams[primitiveId],
});
}
}
} catch (err) {
console.warn('MCP primitive call error', err);
@@ -196,7 +202,7 @@ export const McpRequestPane: FC<Props> = ({
useEffect(() => {
if (isConnected) {
latestPayloadPatcherRef.current(requestId, { params: mcpParams, url: activeRequest.url });
// latestPayloadPatcherRef.current(requestId, { params: mcpParams, url: activeRequest.url });
}
}, [activeRequest.url, mcpParams, latestPayloadPatcherRef, requestId, isConnected]);
@@ -206,6 +212,36 @@ export const McpRequestPane: FC<Props> = ({
}
}, [activeRequest.url, latestRequestPayloadRef, isConnected]);
useEffect(() => {
const getMcpAppResource = async (toolName: string) => {
const resourceData = await window.main.mcp.ext.app.getResourceData({
requestId,
toolName,
});
if (resourceData) {
setAppResourceData(resourceData);
}
};
if (selectedPrimitiveItem?.type === 'tools') {
const toolMeta = selectedPrimitiveItem._meta;
if (toolMeta && 'ui' in toolMeta) {
// Check if the tool has a UI component and visible to the client
const result = McpUiToolMetaSchema.safeParse(toolMeta.ui);
if (result.success) {
const visibility = result.data.visibility;
const resourceUri = result.data.resourceUri;
// visibility values: "model": Tool visible to and callable by the agent, "app": Tool callable by the app from this server only
const shouldRenderMcpApp = !visibility || visibility.includes('model');
if (shouldRenderMcpApp && resourceUri) {
getMcpAppResource(selectedPrimitiveItem.name);
}
}
}
}
// clear app resource data when primitive item changes
setAppResourceData(null);
}, [selectedPrimitiveItem, requestId]);
return (
<Pane type="request">
<header className="pane__header theme--pane__header items-stretch!">
@@ -238,6 +274,12 @@ export const McpRequestPane: FC<Props> = ({
>
<span>Params</span>
</Tab>
<Tab
className="flex h-full shrink-0 cursor-pointer items-center justify-between gap-2 px-3 py-1 text-(--hl) outline-hidden transition-colors duration-300 select-none hover:bg-(--hl-sm) hover:text-(--color-font) focus:bg-(--hl-sm) aria-selected:bg-(--hl-xs) aria-selected:text-(--color-font) aria-selected:hover:bg-(--hl-sm) aria-selected:focus:bg-(--hl-sm)"
id="mcp-app"
>
<span>MCP App</span>
</Tab>
{!isStdio && (
<Tab
className="flex h-full shrink-0 cursor-pointer items-center justify-between gap-2 px-3 py-1 text-(--hl) outline-hidden transition-colors duration-300 select-none hover:bg-(--hl-sm) hover:text-(--color-font) focus:bg-(--hl-sm) aria-selected:bg-(--hl-xs) aria-selected:text-(--color-font) aria-selected:hover:bg-(--hl-sm) aria-selected:focus:bg-(--hl-sm)"
@@ -398,6 +440,26 @@ export const McpRequestPane: FC<Props> = ({
<TabPanel className="flex w-full flex-1 flex-col overflow-hidden" id="roots">
<McpRootsPanel request={activeRequest} readyState={readyState} />
</TabPanel>
<TabPanel className="flex w-full flex-1 flex-col overflow-hidden" id="mcp-app">
{appResourceData ? (
<McpAppEmbeddedView
appResourceData={appResourceData}
requestId={requestId}
onInteraction={data => {
console.log('🎯 App interaction:', data);
// TODO: Re-execute tool with interaction data
}}
/>
) : (
<div className="flex h-full w-full flex-col items-center p-5 text-center">
<p className="notice info text-md no-margin-top w-full">
{selectedPrimitiveItem?.type === 'tools'
? 'This tool does not have an associated MCP App UI component.'
: 'Select a tool primitive with an MCP App UI component from the list to start.'}
</p>
</div>
)}
</TabPanel>
</Tabs>
</Pane>
);