Skip to content

Commit 7ac4fe5

Browse files
Add MCP setup to app menu (#76)
1 parent 57ea6c8 commit 7ac4fe5

4 files changed

Lines changed: 475 additions & 24 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Set `WISP_OUTPUT_DIR` to override where recordings are written. When unset, Wisp
8080

8181
### Local MCP bridge
8282

83-
Enable **Local MCP Bridge** from the Wisp library screen to expose the visible transcript through a local IPC endpoint. By default it binds to `http://127.0.0.1:8765/conversation`; set `WISP_IPC_ADDR=127.0.0.1:9001` to override the address while developing. Set `WISP_IPC_TOKEN` to require `Authorization: Bearer <token>` on IPC requests.
83+
Choose **Wisp → MCP Setup…** (or press <kbd>⌘,</kbd>) to open the guided setup window. From there you can enable the Local MCP Bridge and copy the bundled server path or ready-to-paste JSON for Claude and OpenCode. The bridge exposes the visible transcript through a local IPC endpoint. By default it binds to `http://127.0.0.1:8765/conversation`; set `WISP_IPC_ADDR=127.0.0.1:9001` to override the address while developing. Set `WISP_IPC_TOKEN` to require `Authorization: Bearer <token>` on IPC requests; copied client configs include the current address and, when set, the token.
8484

8585
MCP hosts should run the bundled `wisp-mcp` binary over stdio, for example `/Applications/Wisp.app/Contents/MacOS/wisp-mcp`. The bridge provides the `ask_current_conversation` tool, fetches the current Wisp transcript from the IPC endpoint, and returns context for the host LLM to answer questions such as `いまの話ってどういうこと?`.
8686

apps/wisp-desktop/src/app_menu.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
//! macOS menu bar: application menu (About, Start/Stop Recording, Quit)
2-
//! plus the Cmd+Q and Cmd+R shortcuts.
1+
//! macOS menu bar: application menu (About, MCP Setup, recording, export,
2+
//! and Quit) plus their keyboard shortcuts.
33
4+
use std::cell::RefCell;
45
use std::path::PathBuf;
6+
use std::rc::Rc;
57
use std::sync::Arc;
68
use std::time::Duration;
79

@@ -10,6 +12,7 @@ use gpui::{App, Entity, KeyBinding, Menu, MenuItem, actions};
1012
use crate::about_view;
1113
use crate::app::{AppModel, SessionState, View};
1214
use crate::library::SharedStorage;
15+
use crate::mcp_setup_view;
1316
use crate::session_runner::SessionRunner;
1417
use crate::session_updates::apply_update;
1518
use crate::transcript_export::{self, suggested_export_name};
@@ -19,6 +22,7 @@ actions!(
1922
[
2023
Quit,
2124
About,
25+
OpenMcpSetup,
2226
ToggleRecording,
2327
CopyTranscript,
2428
ExportTranscript
@@ -31,6 +35,7 @@ pub fn configure(
3135
runner: Arc<SessionRunner>,
3236
storage: SharedStorage,
3337
model: Entity<AppModel>,
38+
on_set_local_mcp_enabled: Arc<dyn Fn(bool, &mut App)>,
3439
data_dir: PathBuf,
3540
recordings_dir: PathBuf,
3641
) {
@@ -47,6 +52,31 @@ pub fn configure(
4752
about_view::open(cx);
4853
});
4954

55+
let model_for_mcp_setup = model.clone();
56+
let mcp_setup_window: Rc<RefCell<Option<gpui::WindowHandle<mcp_setup_view::McpSetupView>>>> =
57+
Rc::new(RefCell::new(None));
58+
cx.on_action(move |_: &OpenMcpSetup, cx| {
59+
let existing = *mcp_setup_window.borrow();
60+
if let Some(handle) = existing {
61+
if cx.active_window() == Some(handle.into()) {
62+
return;
63+
}
64+
if handle
65+
.update(cx, |_, window, _| window.activate_window())
66+
.is_ok()
67+
{
68+
return;
69+
}
70+
}
71+
72+
let window = mcp_setup_view::open(
73+
cx,
74+
model_for_mcp_setup.clone(),
75+
on_set_local_mcp_enabled.clone(),
76+
);
77+
*mcp_setup_window.borrow_mut() = Some(window);
78+
});
79+
5080
// Start/stop recording straight from the menu bar (and Cmd+R), so the
5181
// user doesn't have to reach for the in-window Record button. Reuses the
5282
// same state machine as that button via `toggle_recording`.
@@ -85,6 +115,7 @@ pub fn configure(
85115

86116
cx.bind_keys([
87117
KeyBinding::new("cmd-q", Quit, None),
118+
KeyBinding::new("cmd-,", OpenMcpSetup, None),
88119
KeyBinding::new("cmd-r", ToggleRecording, None),
89120
KeyBinding::new("cmd-shift-c", CopyTranscript, None),
90121
KeyBinding::new("cmd-shift-e", ExportTranscript, None),
@@ -136,6 +167,8 @@ fn build_menus(record_label: &'static str) -> Vec<Menu> {
136167
items: vec![
137168
MenuItem::action("About Wisp", About),
138169
MenuItem::separator(),
170+
MenuItem::action("MCP Setup…", OpenMcpSetup),
171+
MenuItem::separator(),
139172
MenuItem::action(record_label, ToggleRecording),
140173
MenuItem::separator(),
141174
MenuItem::action("Copy Transcript", CopyTranscript),
@@ -176,10 +209,37 @@ fn graceful_stop_session(
176209
}
177210

178211
let updates = runner.wait_for_idle(Duration::from_secs(5));
179-
let _ = model.update(cx, |m, cx| {
212+
model.update(cx, |m, cx| {
180213
for update in updates {
181214
apply_update(update, m, storage);
182215
}
183216
cx.notify();
184217
});
185218
}
219+
220+
#[cfg(test)]
221+
mod tests {
222+
use gpui::MenuItem;
223+
224+
use super::{OpenMcpSetup, build_menus};
225+
226+
#[test]
227+
fn menu_exposes_mcp_setup_action() {
228+
let menus = build_menus("Start Recording");
229+
let menu = menus.first().expect("Wisp menu");
230+
231+
let actions = menu
232+
.items
233+
.iter()
234+
.filter_map(|item| match item {
235+
MenuItem::Action { name, action, .. } if name.as_ref() == "MCP Setup…" => {
236+
Some(action.as_ref())
237+
},
238+
_ => None,
239+
})
240+
.collect::<Vec<_>>();
241+
242+
assert_eq!(actions.len(), 1);
243+
assert!(actions[0].as_any().is::<OpenMcpSetup>());
244+
}
245+
}

apps/wisp-desktop/src/main.rs

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ mod app;
3939
mod app_menu;
4040
mod ipc_server;
4141
mod library;
42+
mod mcp_setup_view;
4243
mod permissions;
4344
mod session_runner;
4445
mod session_updates;
@@ -108,15 +109,31 @@ fn main() {
108109
start_local_mcp_bridge(&ipc_handle, &ipc_snapshot, &model, cx);
109110
}
110111

112+
let on_set_local_mcp_enabled: Arc<dyn Fn(bool, &mut App)> = {
113+
let ipc_handle = ipc_handle.clone();
114+
let ipc_snapshot = ipc_snapshot.clone();
115+
let model = model.clone();
116+
let data_dir = data_dir.clone();
117+
Arc::new(move |enabled, cx| {
118+
set_local_mcp_bridge_enabled(
119+
enabled,
120+
&ipc_handle,
121+
&ipc_snapshot,
122+
&model,
123+
&data_dir,
124+
cx,
125+
);
126+
})
127+
};
128+
111129
let window = open_main_window(
112130
cx,
113131
window_options,
114132
MainWindowDeps {
115133
runner: runner.clone(),
116134
storage: storage.clone(),
117135
model: model.clone(),
118-
ipc_handle: ipc_handle.clone(),
119-
ipc_snapshot: ipc_snapshot.clone(),
136+
on_set_local_mcp_enabled: on_set_local_mcp_enabled.clone(),
120137
data_dir: data_dir.clone(),
121138
recordings_dir: recordings_dir.clone(),
122139
},
@@ -127,6 +144,7 @@ fn main() {
127144
runner.clone(),
128145
storage.clone(),
129146
model.clone(),
147+
on_set_local_mcp_enabled,
130148
data_dir.clone(),
131149
recordings_dir,
132150
);
@@ -142,8 +160,7 @@ struct MainWindowDeps {
142160
runner: Arc<SessionRunner>,
143161
storage: SharedStorage,
144162
model: Entity<AppModel>,
145-
ipc_handle: Arc<Mutex<Option<ipc_server::IpcServer>>>,
146-
ipc_snapshot: ipc_server::SharedSnapshot,
163+
on_set_local_mcp_enabled: Arc<dyn Fn(bool, &mut App)>,
147164
data_dir: PathBuf,
148165
recordings_dir: PathBuf,
149166
}
@@ -157,8 +174,7 @@ fn open_main_window(
157174
runner,
158175
storage,
159176
model,
160-
ipc_handle,
161-
ipc_snapshot,
177+
on_set_local_mcp_enabled,
162178
data_dir,
163179
recordings_dir,
164180
} = deps;
@@ -175,11 +191,8 @@ fn open_main_window(
175191
let storage_for_open_history = storage.clone();
176192
let data_for_toggle = data_dir.clone();
177193
let data_for_download = data_dir.clone();
178-
let data_for_local_mcp = data_dir.clone();
179194
let recordings_for_toggle = recordings_dir.clone();
180195
let runner_for_toggle = runner.clone();
181-
let ipc_for_local_mcp = ipc_handle.clone();
182-
let ipc_snapshot_for_local_mcp = ipc_snapshot.clone();
183196
let (transcript_list, follow_transcript) = new_transcript_list_state();
184197
let view = TranscriptView {
185198
app: model.clone(),
@@ -238,13 +251,8 @@ fn open_main_window(
238251
});
239252
}),
240253
on_toggle_local_mcp: Arc::new(move |_window, cx| {
241-
toggle_local_mcp_bridge(
242-
&ipc_for_local_mcp,
243-
&ipc_snapshot_for_local_mcp,
244-
&model_for_local_mcp,
245-
&data_for_local_mcp,
246-
cx,
247-
);
254+
let enabled = !model_for_local_mcp.read(cx).local_mcp.enabled;
255+
on_set_local_mcp_enabled(enabled, cx);
248256
}),
249257
};
250258
// Re-render whenever the underlying model changes.
@@ -357,17 +365,21 @@ fn spawn_ipc_snapshot_sync(
357365
.detach();
358366
}
359367

360-
fn toggle_local_mcp_bridge(
368+
fn set_local_mcp_bridge_enabled(
369+
enabled: bool,
361370
ipc_handle: &Arc<Mutex<Option<ipc_server::IpcServer>>>,
362371
snapshot: &ipc_server::SharedSnapshot,
363372
model: &Entity<AppModel>,
364373
data_dir: &std::path::Path,
365374
cx: &mut App,
366375
) {
367-
if model.read(cx).local_mcp.enabled {
368-
stop_local_mcp_bridge(ipc_handle, model, cx);
369-
} else {
376+
if enabled {
377+
if model.read(cx).local_mcp.running {
378+
return;
379+
}
370380
start_local_mcp_bridge(ipc_handle, snapshot, model, cx);
381+
} else {
382+
stop_local_mcp_bridge(ipc_handle, model, cx);
371383
}
372384
save_local_mcp_settings(data_dir, model, cx);
373385
}

0 commit comments

Comments
 (0)