Skip to content

Commit a1d9599

Browse files
its-mashclaude
andauthored
feat: support default values for input definitions (#70)
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 68c292c commit a1d9599

4 files changed

Lines changed: 586 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Codebase Search
6+
7+
This repository is indexed for semantic code search via the `claude-context` MCP server. Prefer `mcp__claude-context__search_code` (with path `D:\mcpmux`) over Grep/Glob when looking for implementations, understanding how features work, or finding related code across the codebase. Use Grep/Glob for exact string matches or filename patterns. If search returns an indexing error, re-index with `mcp__claude-context__index_codebase` first.
8+
9+
## What is McpMux
10+
11+
McpMux is a desktop app + local gateway that lets users configure MCP servers once and connect every AI client (Cursor, Claude Desktop, VS Code, Windsurf) through a single `localhost:45818` endpoint. Credentials are encrypted in the OS keychain instead of plain-text JSON files.
12+
13+
## Repository Structure
14+
15+
This is a multi-project workspace with 6 independent projects (not a unified monorepo):
16+
17+
| Project | Tech | Purpose |
18+
|---------|------|---------|
19+
| `mcp-mux/` | Tauri 2 (Rust + React 19), pnpm workspace | Desktop app + local MCP gateway |
20+
| `mcpmux.bundler/` | Cloudflare Worker | GitHub webhook receiver that processes server definitions into D1/R2 |
21+
| `mcpmux.serverhub.api/` | Cloudflare Worker (Hono) | REST API for server registry discovery (KV -> D1 -> R2 fallback chain) |
22+
| `mcpmux.discover.ui/` | Next.js 16, shadcn/ui, Tailwind 4 | Web UI for browsing the server registry |
23+
| `mcp-servers/` | JSON + AJV validation | Community MCP server definitions repository |
24+
| `mcpmux.space/` | Docs | Documentation and design space |
25+
26+
## mcp-mux (Desktop App) - Main Project
27+
28+
### Build & Dev Commands
29+
30+
All commands run from `mcp-mux/`:
31+
32+
```bash
33+
pnpm setup # First-time dev environment setup (PowerShell)
34+
pnpm dev # Tauri desktop app dev mode (Rust + React hot-reload)
35+
pnpm dev:web # Web UI only (Vite, no Rust)
36+
pnpm build # Production Tauri build (all platforms)
37+
```
38+
39+
### Testing
40+
41+
```bash
42+
pnpm test # All tests (Rust + TypeScript)
43+
pnpm test:rust # cargo nextest run --workspace
44+
pnpm test:rust:unit # cargo nextest run --workspace --lib
45+
pnpm test:rust:int # cargo nextest run -p tests
46+
pnpm test:rust:doc # cargo test --workspace --doc
47+
pnpm test:ts # vitest run -c tests/ts/vitest.config.ts
48+
pnpm test:ts:watch # vitest watch mode
49+
pnpm test:e2e # WebDriver IO desktop E2E (needs MCPMUX_REGISTRY_URL)
50+
pnpm test:e2e:file # Single E2E spec: pnpm test:e2e:file -- tests/e2e/specs/foo.ts
51+
pnpm test:e2e:grep # E2E by name: pnpm test:e2e:grep -- "test name"
52+
pnpm test:e2e:web # Playwright web UI E2E
53+
pnpm test:coverage # cargo llvm-cov + vitest coverage
54+
```
55+
56+
### Linting & Validation
57+
58+
```bash
59+
pnpm validate # Full check: cargo fmt + clippy + check + eslint + typecheck
60+
pnpm lint # ESLint (recursive) + cargo clippy --workspace -- -D warnings
61+
pnpm lint:fix # Auto-fix lint issues
62+
pnpm format # prettier --write . && cargo fmt --all
63+
pnpm format:check # Check formatting without modifying
64+
pnpm typecheck # TypeScript type checking (recursive)
65+
```
66+
67+
### Rust Crate Architecture
68+
69+
The Cargo workspace has 4 library crates + 1 app crate + 1 test crate:
70+
71+
- **mcpmux-core** (`crates/mcpmux-core/`) - Domain layer: entities (Space, InstalledServer, FeatureSet, Client), repository traits, domain services, application services with event emission, and the central EventBus
72+
- **mcpmux-gateway** (`crates/mcpmux-gateway/`) - Axum HTTP gateway: routes MCP calls to correct servers, manages OAuth 2.1+PKCE token refresh, filters tools/resources/prompts based on FeatureSets, per-client access key auth, server connection pooling
73+
- **mcpmux-storage** (`crates/mcpmux-storage/`) - SQLite persistence with AES-256-GCM field-level encryption via ring, typed credential rows (per-token encryption), DPAPI key storage on Windows (`keychain_dpapi.rs`), OS keychain on macOS/Linux via keyring crate, zeroize for secure memory clearing
74+
- **mcpmux-mcp** (`crates/mcpmux-mcp/`) - MCP protocol client management using rmcp SDK
75+
- **apps/desktop/src-tauri** - Tauri 2 app shell, Tauri commands, system tray, deep-link handler (`mcpmux://`)
76+
- **tests/rust** - Integration test crate
77+
78+
Key patterns: event-driven architecture (EventBus), repository pattern (trait-based storage abstraction), service layer pattern with DI via ApplicationServices builders.
79+
80+
### Frontend Architecture
81+
82+
- **Entry**: `apps/desktop/src/main.tsx` -> `App.tsx`
83+
- **State**: Zustand store (`stores/appStore.ts`)
84+
- **Hooks**: `useServerManager` (server CRUD), `useSpaces` (workspace switching), `useDomainEvents` (Rust event listeners), `useDataSync` (data synchronization)
85+
- **UI**: React 19 + Tailwind CSS + Lucide icons + Monaco Editor (config editing)
86+
- **Path aliases**: `@/` -> `src/`, `@mcpmux/ui` -> shared UI package
87+
88+
### Data Flow
89+
90+
```
91+
AI Clients -> McpMux Gateway (localhost:45818/mcp) -> MCP Servers (stdio/HTTP)
92+
|
93+
Authenticates (access keys)
94+
Routes (per-space server config)
95+
Filters (FeatureSet permissions)
96+
OAuth token refresh (automatic)
97+
Credentials (DPAPI files on Windows / OS Keychain on macOS+Linux + encrypted SQLite)
98+
```
99+
100+
### Prerequisites
101+
102+
Rust 1.75+, Node.js 20+, pnpm 9+. Linux: `gnome-keyring libsecret-1-dev librsvg2-dev pkg-config`.
103+
104+
### Code Style
105+
106+
- Rust: 100 char max width, 4-space indent, `clippy` with `avoid-breaking-exported-api = false`
107+
- TypeScript/JSX: Prettier with single quotes, 2-space indent, 100 char width, trailing commas (es5), Tailwind CSS plugin
108+
- Commits require `Signed-off-by` line (use `git commit -s`)
109+
110+
## mcp-servers (Server Definitions Repository)
111+
112+
All commands run from `mcp-servers/` (sibling repo):
113+
114+
```bash
115+
pnpm install # Install dependencies (vitest, ajv, glob)
116+
pnpm validate:all # Validate all server definitions against JSON schema
117+
pnpm validate <file> # Validate specific server definition file(s)
118+
pnpm build # Build registry bundle (bundle/bundle.json)
119+
pnpm test # Run all tests (schema validation, consistency, categories, bundle)
120+
```
121+
122+
The JSON schema at `schemas/server-definition.schema.json` defines the structure for server definitions in `servers/`. Input definitions support: `id`, `label`, `type`, `required`, `secret`, `description`, `default`, `placeholder`, and `obtain` (with `url`, `instructions`, `button_label`).
123+
124+
## Cloudflare Workers
125+
126+
Each worker is an independent project with its own `package.json`:
127+
128+
**mcpmux.serverhub.api/** - Hono-based registry API:
129+
```bash
130+
cd mcpmux.serverhub.api && pnpm dev # wrangler dev
131+
cd mcpmux.serverhub.api && pnpm test # vitest (cloudflare pool)
132+
```
133+
134+
**mcpmux.bundler/** - Webhook-triggered bundle processor:
135+
```bash
136+
cd mcpmux.bundler && pnpm dev # wrangler dev
137+
cd mcpmux.bundler && pnpm test # vitest run
138+
```
139+
140+
## mcpmux.discover.ui (Discovery Web UI)
141+
142+
```bash
143+
cd mcpmux.discover.ui && pnpm dev # next dev
144+
cd mcpmux.discover.ui && pnpm test # vitest
145+
cd mcpmux.discover.ui && pnpm test:e2e # playwright
146+
```
147+
148+
## Important Patterns
149+
150+
### Child Process Platform Flags
151+
152+
When spawning child processes (e.g., stdio MCP servers), **always** use `configure_child_process_platform()` from `mcpmux_gateway::pool::transport`. This applies:
153+
- **Windows**: `CREATE_NO_WINDOW` (`0x08000000`) — prevents visible console windows in release builds (where `windows_subsystem = "windows"` means the app is a GUI subsystem process)
154+
- **Unix**: `process_group(0)` — isolates child from parent terminal signals (SIGINT, SIGTSTP)
155+
156+
Note: `tokio::process::Command` already exposes `creation_flags()` (Windows) and `process_group()` (Unix) natively — do **not** import `std::os::unix::process::CommandExt` or `std::os::windows::process::CommandExt` as the traits are unused with Tokio's Command.
157+
158+
### Cross-Platform CI Awareness
159+
160+
The pre-commit hook runs `cargo clippy --workspace -- -D warnings` locally, but `#[cfg(unix)]` / `#[cfg(windows)]` blocks are only compiled on the matching platform. **CI runs on Linux**, so:
161+
- `#[cfg(unix)]` code is only linted in CI, not on a Windows dev machine
162+
- `#[cfg(windows)]` code is only linted locally on Windows, not in CI
163+
- Always validate that platform-conditional code compiles correctly on both platforms before pushing
164+
165+
## CI
166+
167+
GitHub Actions runs on push/PR to main: Rust format + clippy + check, ESLint + typecheck, cargo nextest, vitest, desktop E2E (Windows/macOS/Linux), web E2E (Playwright). Releases use release-please for semantic versioning with multi-platform Tauri builds.

crates/mcpmux-core/src/domain/config.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ impl UserServerEntry {
254254
required: true,
255255
secret: true,
256256
description: None,
257+
default: None,
257258
placeholder: None,
258259
obtain_url: None,
259260
obtain_instructions: None,
@@ -444,6 +445,7 @@ mod tests {
444445
required: false,
445446
secret: false,
446447
description: Some("Custom description".to_string()),
448+
default: None,
447449
placeholder: None,
448450
obtain_url: None,
449451
obtain_instructions: None,
@@ -649,4 +651,133 @@ mod tests {
649651
// Explicit OAuth should not be overridden
650652
assert!(matches!(def.auth, Some(AuthConfig::Oauth)));
651653
}
654+
655+
#[test]
656+
fn test_input_default_value_parsed_from_json() {
657+
let json = r#"{
658+
"mcpServers": {
659+
"test-server": {
660+
"command": "node",
661+
"args": ["server.js"],
662+
"env": {
663+
"LOG_LEVEL": "${input:LOG_LEVEL}"
664+
},
665+
"metadata": {
666+
"inputs": [
667+
{
668+
"id": "LOG_LEVEL",
669+
"label": "Log Level",
670+
"type": "text",
671+
"required": false,
672+
"secret": false,
673+
"default": "info"
674+
}
675+
]
676+
}
677+
}
678+
}
679+
}"#;
680+
681+
let config: UserSpaceConfig = serde_json::from_str(json).unwrap();
682+
let definitions =
683+
config.to_server_definitions("test-space", PathBuf::from("/test/path.json"));
684+
685+
assert_eq!(definitions.len(), 1);
686+
let inputs = &definitions[0].transport.metadata().inputs;
687+
assert_eq!(inputs.len(), 1);
688+
assert_eq!(inputs[0].id, "LOG_LEVEL");
689+
assert_eq!(inputs[0].default, Some("info".to_string()));
690+
}
691+
692+
#[test]
693+
fn test_explicit_input_with_default_takes_precedence_over_autodiscovery() {
694+
let entry = UserServerEntry {
695+
command: Some("node".to_string()),
696+
args: None,
697+
env: Some(HashMap::from([(
698+
"LOG_LEVEL".to_string(),
699+
"${input:LOG_LEVEL}".to_string(),
700+
)])),
701+
url: None,
702+
headers: None,
703+
name: None,
704+
description: None,
705+
icon: None,
706+
alias: None,
707+
auth: None,
708+
metadata: Some(UserServerMetadata {
709+
inputs: Some(vec![InputDefinition {
710+
id: "LOG_LEVEL".to_string(),
711+
label: "Log Level".to_string(),
712+
r#type: "text".to_string(),
713+
required: false,
714+
secret: false,
715+
description: None,
716+
default: Some("info".to_string()),
717+
placeholder: None,
718+
obtain_url: None,
719+
obtain_instructions: None,
720+
}]),
721+
publisher: None,
722+
}),
723+
};
724+
725+
let (_, inputs) = entry.resolve_transport_and_inputs();
726+
727+
assert_eq!(inputs.len(), 1);
728+
assert_eq!(inputs[0].id, "LOG_LEVEL");
729+
assert_eq!(inputs[0].default, Some("info".to_string()));
730+
// Should use explicit definition's type, not auto-discovered "password"
731+
assert_eq!(inputs[0].r#type, "text");
732+
assert!(!inputs[0].required);
733+
assert!(!inputs[0].secret);
734+
}
735+
736+
#[test]
737+
fn test_auto_discovered_inputs_have_no_default() {
738+
let entry = UserServerEntry {
739+
command: Some("node".to_string()),
740+
args: None,
741+
env: Some(HashMap::from([(
742+
"API_KEY".to_string(),
743+
"${input:API_KEY}".to_string(),
744+
)])),
745+
url: None,
746+
headers: None,
747+
name: None,
748+
description: None,
749+
icon: None,
750+
alias: None,
751+
auth: None,
752+
metadata: None,
753+
};
754+
755+
let (_, inputs) = entry.resolve_transport_and_inputs();
756+
757+
assert_eq!(inputs.len(), 1);
758+
assert_eq!(inputs[0].id, "API_KEY");
759+
assert_eq!(inputs[0].default, None);
760+
}
761+
762+
#[test]
763+
fn test_input_default_serializes_roundtrip() {
764+
let input = InputDefinition {
765+
id: "PORT".to_string(),
766+
label: "Port".to_string(),
767+
r#type: "number".to_string(),
768+
required: false,
769+
secret: false,
770+
description: None,
771+
default: Some("8080".to_string()),
772+
placeholder: None,
773+
obtain_url: None,
774+
obtain_instructions: None,
775+
};
776+
777+
let json = serde_json::to_string(&input).unwrap();
778+
let deserialized: InputDefinition = serde_json::from_str(&json).unwrap();
779+
780+
assert_eq!(deserialized.id, "PORT");
781+
assert_eq!(deserialized.default, Some("8080".to_string()));
782+
}
652783
}

crates/mcpmux-core/src/domain/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ pub struct InputDefinition {
146146
#[serde(default)]
147147
pub secret: bool,
148148
pub description: Option<String>,
149+
pub default: Option<String>,
149150
pub placeholder: Option<String>,
150151

151152
// Additional helpful metadata for acquiring credentials

0 commit comments

Comments
 (0)