1use std::ffi::OsString;
7
8use clap::Parser;
9use serde_json::{Map, Value};
10
11use crate::bake::require_baked;
12use crate::cache::cache_key_for;
13use crate::cli::args::GlobalArgs;
14use crate::cli::bake::handle_bake;
15use crate::cli::dynamic::{parse_tool_args, read_stdin_json};
16use crate::cli::list::{
17 apply_list_options, describe_tool, filter_by_search, list_commands, maybe_tool_help,
18 ListOptions, ListStyle,
19};
20use crate::cli::{global_option_sets, split_at_subcommand};
21use crate::error::{Error, Result};
22use crate::filter::filter_commands;
23use crate::graphql::{execute_graphql, extract_graphql_commands, load_graphql_schema};
24use crate::mcp::{
25 call_tool_http, call_tool_stdio, fetch_mcp_tools_http, fetch_mcp_tools_stdio,
26 tools_to_commands, TransportMode,
27};
28use crate::model::{BakeConfig, ListDetail};
29use crate::oauth::{
30 clear_oauth_credentials, discovery_url_from_args, oauth_wanted, options_from_args,
31 setup_from_args, OAuthReady,
32};
33use crate::openapi::{
34 execute_openapi, extract_openapi_commands, load_openapi_spec, resolve_base_url,
35};
36use crate::output::output_result;
37use crate::tools_index::{tools_to_light_commands, try_commands_from_index};
38use crate::usage::{record_usage, source_hash_for};
39
40pub fn dispatch(argv: Vec<OsString>) -> Result<()> {
41 if argv.first().and_then(|a| a.to_str()) == Some("bake") {
42 return handle_bake(&argv[1..]);
43 }
44 if let Some(first) = argv.first().and_then(|a| a.to_str()) {
45 if let Some(name) = first.strip_prefix('@') {
46 return run_baked(name, &argv[1..]);
47 }
48 }
49
50 dispatch_impl(argv, None)
51}
52
53fn run_baked(name: &str, rest: &[OsString]) -> Result<()> {
54 let cfg = require_baked(name)?;
55 let bake_config = cfg.bake_config();
56 let mut synthetic: Vec<OsString> = cfg.to_argv().into_iter().map(OsString::from).collect();
57 synthetic.extend_from_slice(rest);
58 dispatch_impl(synthetic, Some(bake_config))
59}
60
61fn dispatch_impl(argv: Vec<OsString>, bake_config: Option<BakeConfig>) -> Result<()> {
62 let (value_opts, bool_opts) = global_option_sets();
63 let (global_argv, tool_argv) = split_at_subcommand(&argv, &value_opts, &bool_opts);
64
65 let mut clap_argv = vec![OsString::from("skiff")];
66 clap_argv.extend(global_argv);
67
68 let mut pre_args = GlobalArgs::try_parse_from(&clap_argv).map_err(|e| {
69 let msg = e.to_string();
70 if msg.contains("help")
71 || e.kind() == clap::error::ErrorKind::DisplayHelp
72 || e.kind() == clap::error::ErrorKind::DisplayVersion
73 {
74 e.print().ok();
75 Error::usage("__printed__")
76 } else {
77 Error::usage(msg)
78 }
79 })?;
80
81 pre_args.apply_agent_defaults();
82
83 if pre_args.spool_clean {
84 let n = crate::spool::clean_spool(crate::spool::DEFAULT_SPOOL_TTL_SECS)?;
85 println!("Removed {n} expired spool file(s)");
86 return Ok(());
87 }
88
89 if pre_args.search_pattern.is_some() {
90 pre_args.list_commands = true;
91 }
92
93 if pre_args.describe.is_some() {
94 pre_args.list_commands = false;
95 }
96
97 if pre_args.oauth_clear {
99 let url = discovery_url_from_args(&pre_args)?;
100 clear_oauth_credentials(&url)?;
101 println!("Cleared OAuth credentials for {url}");
102 return Ok(());
103 }
104
105 let _ = options_from_args(&pre_args)?;
107
108 let tool_argv: Vec<String> = tool_argv
109 .iter()
110 .map(|a| a.to_string_lossy().into_owned())
111 .collect();
112
113 #[cfg(unix)]
115 {
116 if pre_args.session_list {
117 return dispatch_session_list(&pre_args);
118 }
119 if let Some(name) = &pre_args.session_stop {
120 return crate::session::session_stop(name);
121 }
122 if let Some(name) = &pre_args.session_start {
123 return dispatch_session_start(&pre_args, name);
124 }
125 if let Some(name) = &pre_args.session {
126 return dispatch_session(&pre_args, name, &tool_argv, bake_config.as_ref());
127 }
128 }
129 #[cfg(not(unix))]
130 {
131 if pre_args.session_list
132 || pre_args.session_stop.is_some()
133 || pre_args.session_start.is_some()
134 || pre_args.session.is_some()
135 {
136 return Err(crate::session::sessions_unsupported());
137 }
138 }
139
140 let modes = [
141 pre_args.spec.is_some(),
142 pre_args.mcp.is_some(),
143 pre_args.mcp_stdio.is_some(),
144 pre_args.graphql.is_some(),
145 ];
146 let active = modes.iter().filter(|x| **x).count();
147 if active == 0 {
148 print_help();
149 return Err(Error::usage(
150 "one of --spec, --mcp, --mcp-stdio, or --graphql is required.",
151 ));
152 }
153 if active > 1 {
154 return Err(Error::usage(
155 "--spec, --mcp, --mcp-stdio, and --graphql are mutually exclusive.",
156 ));
157 }
158
159 if let Some(cmd) = &pre_args.mcp_stdio {
160 if oauth_wanted(&pre_args) {
161 return Err(Error::usage(
162 "OAuth is not supported with --mcp-stdio (HTTP discovery required)",
163 ));
164 }
165 return dispatch_mcp_stdio(&pre_args, cmd, &tool_argv, bake_config.as_ref());
166 }
167
168 if let Some(url) = &pre_args.mcp {
169 return dispatch_mcp_http(&pre_args, url, &tool_argv, bake_config.as_ref());
170 }
171
172 if let Some(spec) = &pre_args.spec {
173 return dispatch_openapi(&pre_args, spec, &tool_argv, bake_config.as_ref());
174 }
175
176 if let Some(url) = &pre_args.graphql {
177 return dispatch_graphql(&pre_args, url, &tool_argv, bake_config.as_ref());
178 }
179
180 Err(Error::usage("no source mode selected"))
181}
182
183fn apply_bake_filter(
184 commands: Vec<crate::CommandDef>,
185 bake: Option<&BakeConfig>,
186) -> Vec<crate::CommandDef> {
187 match bake {
188 Some(b) => filter_commands(commands, &b.include, &b.exclude, &b.methods),
189 None => commands,
190 }
191}
192
193fn mcp_discovery_commands(
195 tools: Option<&[Value]>,
196 cache_key: &str,
197 pre: &GlobalArgs,
198 bake: Option<&BakeConfig>,
199) -> Result<Vec<crate::CommandDef>> {
200 let detail = pre.list_detail();
201 let light = matches!(detail, ListDetail::Names | ListDetail::Brief);
202 let tools_key = format!("{cache_key}_tools");
203 let require_descs = matches!(detail, ListDetail::Brief);
204
205 if light && !pre.refresh {
206 if let Some(cmds) = try_commands_from_index(
207 &tools_key,
208 pre.cache_ttl,
209 pre.search_pattern.as_deref(),
210 require_descs,
211 )? {
212 return Ok(apply_bake_filter(cmds, bake));
213 }
214 }
215
216 let tools = tools.ok_or_else(|| Error::runtime("internal: tools required when index miss"))?;
217 let mut commands = if light {
218 apply_bake_filter(tools_to_light_commands(tools), bake)
219 } else {
220 apply_bake_filter(tools_to_commands(tools), bake)
221 };
222 if let Some(pat) = &pre.search_pattern {
223 commands = filter_by_search(commands, pat);
224 }
225 Ok(commands)
226}
227
228fn emit_mcp_list(
229 commands: Vec<crate::CommandDef>,
230 pre: &GlobalArgs,
231 src_hash: &str,
232 list_opts: &ListOptions,
233) -> Result<()> {
234 if let Some(pat) = &pre.search_pattern {
235 if commands.is_empty() {
236 if pre.json_output || pre.agent {
237 list_commands(&commands, list_opts)?;
238 } else {
239 println!("\nNo tools matching '{pat}'.");
240 }
241 return Ok(());
242 }
243 if !pre.quiet_list() {
244 println!("\nTools matching '{pat}':");
245 }
246 } else if !pre.quiet_list() {
247 println!("\nAvailable tools:");
248 }
249 let commands = apply_list_options(commands, src_hash, pre.sort.as_deref(), pre.top);
252 list_commands(&commands, list_opts)
253}
254
255fn dispatch_openapi(
256 pre: &GlobalArgs,
257 spec_source: &str,
258 remaining: &[String],
259 bake: Option<&BakeConfig>,
260) -> Result<()> {
261 let mut auth = pre.parse_auth_headers()?;
262 let rt = tokio::runtime::Builder::new_multi_thread()
263 .enable_all()
264 .build()
265 .map_err(|e| Error::runtime(e.to_string()))?;
266 let oauth = rt.block_on(setup_from_args(pre))?;
267 if let Some(ref o) = oauth {
268 let token = rt.block_on(o.access_token())?;
269 auth.retain(|(k, _)| !k.eq_ignore_ascii_case("authorization"));
270 auth.push(("Authorization".into(), format!("Bearer {token}")));
271 }
272
273 let src_hash = source_hash_for(spec_source);
274 let spec = load_openapi_spec(
275 spec_source,
276 &auth,
277 pre.cache_key.as_deref(),
278 Some(pre.cache_ttl),
279 pre.refresh,
280 )?;
281 let mut commands = extract_openapi_commands(&spec);
282 commands = apply_bake_filter(commands, bake);
283
284 let list_opts = ListOptions::from_global(pre, ListStyle::OpenApi);
285
286 if let Some(name) = &pre.describe {
287 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
288 return describe_tool(&commands, name, pre);
289 }
290
291 if pre.list_commands {
292 if let Some(pat) = &pre.search_pattern {
293 commands = filter_by_search(commands, pat);
294 if commands.is_empty() {
295 if pre.json_output || pre.agent {
296 list_commands(&commands, &list_opts)?;
297 } else {
298 println!("\nNo tools matching '{pat}'.");
299 }
300 return Ok(());
301 }
302 if !pre.quiet_list() {
303 println!("\nTools matching '{pat}':");
304 }
305 }
306 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
307 return list_commands(&commands, &list_opts);
308 }
309
310 if remaining.is_empty() {
311 print_help();
312 eprintln!("\nUse --list to see all available commands.");
313 return Err(Error::usage("__printed__"));
314 }
315
316 if maybe_tool_help(&commands, remaining, pre)? {
317 return Ok(());
318 }
319
320 let base_url = resolve_base_url(pre.base_url.as_deref(), &spec, spec_source)?;
321 let parsed = match parse_tool_args(&commands, remaining) {
322 Err(Error::Usage(msg)) if msg == "__help__" => return Ok(()),
323 other => other?,
324 };
325
326 execute_openapi(&parsed, &base_url, &auth, &pre.output_options())?;
327 let _ = record_usage(&src_hash, &parsed.command.name);
328 Ok(())
329}
330
331fn dispatch_graphql(
332 pre: &GlobalArgs,
333 url: &str,
334 remaining: &[String],
335 bake: Option<&BakeConfig>,
336) -> Result<()> {
337 let mut auth = pre.parse_auth_headers()?;
338 let rt = tokio::runtime::Builder::new_multi_thread()
339 .enable_all()
340 .build()
341 .map_err(|e| Error::runtime(e.to_string()))?;
342 let oauth = rt.block_on(setup_from_args(pre))?;
343 if let Some(ref o) = oauth {
344 let token = rt.block_on(o.access_token())?;
345 auth.retain(|(k, _)| !k.eq_ignore_ascii_case("authorization"));
346 auth.push(("Authorization".into(), format!("Bearer {token}")));
347 }
348
349 let src_hash = source_hash_for(url);
350 let schema = load_graphql_schema(
351 url,
352 &auth,
353 pre.cache_key.as_deref(),
354 Some(pre.cache_ttl),
355 pre.refresh,
356 )?;
357 let mut commands = extract_graphql_commands(&schema);
358 commands = apply_bake_filter(commands, bake);
359
360 let list_opts = ListOptions::from_global(pre, ListStyle::Graphql);
361
362 if let Some(name) = &pre.describe {
363 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
364 return describe_tool(&commands, name, pre);
365 }
366
367 if pre.list_commands {
368 if let Some(pat) = &pre.search_pattern {
369 commands = filter_by_search(commands, pat);
370 if commands.is_empty() {
371 if pre.json_output || pre.agent {
372 list_commands(&commands, &list_opts)?;
373 } else {
374 println!("\nNo tools matching '{pat}'.");
375 }
376 return Ok(());
377 }
378 if !pre.quiet_list() {
379 println!("\nTools matching '{pat}':");
380 }
381 }
382 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
383 return list_commands(&commands, &list_opts);
384 }
385
386 if remaining.is_empty() {
387 if !pre.quiet_list() {
388 println!("Available operations:");
389 }
390 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
391 list_commands(&commands, &list_opts)?;
392 if !pre.quiet_list() {
393 println!("\nUse --list for the same output, or provide a subcommand.");
394 }
395 return Ok(());
396 }
397
398 if maybe_tool_help(&commands, remaining, pre)? {
399 return Ok(());
400 }
401
402 let parsed = match parse_tool_args(&commands, remaining) {
403 Err(Error::Usage(msg)) if msg == "__help__" => return Ok(()),
404 other => other?,
405 };
406
407 execute_graphql(
408 &parsed,
409 url,
410 &schema,
411 &auth,
412 pre.fields.as_deref(),
413 &pre.output_options(),
414 )?;
415 let usage_key = parsed
416 .command
417 .graphql_field_name
418 .as_deref()
419 .unwrap_or(&parsed.command.name);
420 let _ = record_usage(&src_hash, usage_key);
421 Ok(())
422}
423
424fn dispatch_mcp_http(
425 pre: &GlobalArgs,
426 url: &str,
427 remaining: &[String],
428 bake: Option<&BakeConfig>,
429) -> Result<()> {
430 let auth = pre.parse_auth_headers()?;
431 let transport = TransportMode::parse(&pre.transport)?;
432 let src_hash = source_hash_for(url);
433 let cache_key = pre.cache_key.clone().unwrap_or_else(|| {
434 cache_key_for(&serde_json::json!({
435 "source": url,
436 "is_stdio": false,
437 "auth_headers": auth,
438 "transport": pre.transport,
439 }))
440 });
441
442 let rt = tokio::runtime::Builder::new_multi_thread()
443 .enable_all()
444 .build()
445 .map_err(|e| Error::runtime(e.to_string()))?;
446
447 let oauth: Option<OAuthReady> = rt.block_on(setup_from_args(pre))?;
448 let oauth_ref = oauth.as_ref();
449
450 let list_opts = ListOptions::from_global(pre, ListStyle::Mcp);
451 let tools_key = format!("{cache_key}_tools");
452 let detail = pre.list_detail();
453 let can_light =
454 matches!(detail, ListDetail::Names | ListDetail::Brief) && pre.describe.is_none();
455 let require_descs = matches!(detail, ListDetail::Brief);
456 let discovery = pre.list_commands || remaining.is_empty();
457
458 if discovery && can_light && !pre.refresh {
459 if let Some(cmds) = try_commands_from_index(
460 &tools_key,
461 pre.cache_ttl,
462 pre.search_pattern.as_deref(),
463 require_descs,
464 )? {
465 emit_mcp_list(apply_bake_filter(cmds, bake), pre, &src_hash, &list_opts)?;
466 if remaining.is_empty() && !pre.list_commands && !pre.quiet_list() {
467 println!("\nUse --list for the same output, or provide a subcommand.");
468 }
469 return Ok(());
470 }
471 }
472
473 let tools = rt.block_on(fetch_mcp_tools_http(
474 url,
475 &auth,
476 &cache_key,
477 pre.cache_ttl,
478 pre.refresh,
479 transport,
480 oauth_ref,
481 ))?;
482
483 if let Some(name) = &pre.describe {
484 let commands = apply_bake_filter(tools_to_commands(&tools), bake);
485 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
486 return describe_tool(&commands, name, pre);
487 }
488
489 if discovery {
490 let commands = mcp_discovery_commands(Some(&tools), &cache_key, pre, bake)?;
491 emit_mcp_list(commands, pre, &src_hash, &list_opts)?;
492 if remaining.is_empty() && !pre.list_commands && !pre.quiet_list() {
493 println!("\nUse --list for the same output, or provide a subcommand.");
494 }
495 return Ok(());
496 }
497
498 let commands = apply_bake_filter(tools_to_commands(&tools), bake);
499
500 if maybe_tool_help(&commands, remaining, pre)? {
501 return Ok(());
502 }
503
504 let parsed = match parse_tool_args(&commands, remaining) {
505 Err(Error::Usage(msg)) if msg == "__help__" => return Ok(()),
506 other => other?,
507 };
508
509 let arguments: Map<String, Value> = if parsed.stdin {
510 match read_stdin_json("MCP tool arguments")? {
511 Value::Object(map) => map,
512 other => {
513 return Err(Error::runtime(format!(
514 "MCP --stdin expects a JSON object, got {}",
515 other
516 )));
517 }
518 }
519 } else {
520 parsed.values.into_iter().collect()
521 };
522
523 let tool_name = parsed
524 .command
525 .tool_name
526 .clone()
527 .unwrap_or_else(|| parsed.command.name.clone());
528
529 let data = rt.block_on(call_tool_http(
530 url,
531 &auth,
532 &tool_name,
533 arguments,
534 pre.full_envelope(),
535 transport,
536 oauth_ref,
537 ))?;
538 output_result(data, &pre.output_options())?;
539 let _ = record_usage(&src_hash, &tool_name);
540 Ok(())
541}
542
543fn dispatch_mcp_stdio(
544 pre: &GlobalArgs,
545 command_str: &str,
546 remaining: &[String],
547 bake: Option<&BakeConfig>,
548) -> Result<()> {
549 let env_vars = pre.parse_env_vars()?;
550 let src_hash = source_hash_for(command_str);
551 let cache_key = pre.cache_key.clone().unwrap_or_else(|| {
552 cache_key_for(&serde_json::json!({
553 "source": command_str,
554 "is_stdio": true,
555 "env_vars": env_vars,
556 }))
557 });
558
559 let rt = tokio::runtime::Builder::new_multi_thread()
560 .enable_all()
561 .build()
562 .map_err(|e| Error::runtime(e.to_string()))?;
563
564 let list_opts = ListOptions::from_global(pre, ListStyle::Mcp);
565 let tools_key = format!("{cache_key}_tools");
566 let detail = pre.list_detail();
567 let can_light =
568 matches!(detail, ListDetail::Names | ListDetail::Brief) && pre.describe.is_none();
569 let require_descs = matches!(detail, ListDetail::Brief);
570 let discovery = pre.list_commands || remaining.is_empty();
571
572 if discovery && can_light && !pre.refresh {
573 if let Some(cmds) = try_commands_from_index(
574 &tools_key,
575 pre.cache_ttl,
576 pre.search_pattern.as_deref(),
577 require_descs,
578 )? {
579 emit_mcp_list(apply_bake_filter(cmds, bake), pre, &src_hash, &list_opts)?;
580 if remaining.is_empty() && !pre.list_commands && !pre.quiet_list() {
581 println!("\nUse --list for the same output, or provide a subcommand.");
582 }
583 return Ok(());
584 }
585 }
586
587 let tools = rt.block_on(fetch_mcp_tools_stdio(
588 command_str,
589 &env_vars,
590 &cache_key,
591 pre.cache_ttl,
592 pre.refresh,
593 ))?;
594
595 if let Some(name) = &pre.describe {
596 let commands = apply_bake_filter(tools_to_commands(&tools), bake);
597 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
598 return describe_tool(&commands, name, pre);
599 }
600
601 if discovery {
602 let commands = mcp_discovery_commands(Some(&tools), &cache_key, pre, bake)?;
603 emit_mcp_list(commands, pre, &src_hash, &list_opts)?;
604 if remaining.is_empty() && !pre.list_commands && !pre.quiet_list() {
605 println!("\nUse --list for the same output, or provide a subcommand.");
606 }
607 return Ok(());
608 }
609
610 let commands = apply_bake_filter(tools_to_commands(&tools), bake);
611
612 if maybe_tool_help(&commands, remaining, pre)? {
613 return Ok(());
614 }
615
616 let parsed = match parse_tool_args(&commands, remaining) {
617 Err(Error::Usage(msg)) if msg == "__help__" => return Ok(()),
618 other => other?,
619 };
620
621 let arguments: Map<String, Value> = if parsed.stdin {
622 match read_stdin_json("MCP tool arguments")? {
623 Value::Object(map) => map,
624 other => {
625 return Err(Error::runtime(format!(
626 "MCP --stdin expects a JSON object, got {}",
627 other
628 )));
629 }
630 }
631 } else {
632 parsed.values.into_iter().collect()
633 };
634
635 let tool_name = parsed
636 .command
637 .tool_name
638 .clone()
639 .unwrap_or_else(|| parsed.command.name.clone());
640
641 let data = rt.block_on(call_tool_stdio(
642 command_str,
643 &env_vars,
644 &tool_name,
645 arguments,
646 pre.full_envelope(),
647 ))?;
648 output_result(data, &pre.output_options())?;
649 let _ = record_usage(&src_hash, &tool_name);
650 Ok(())
651}
652
653#[cfg(unix)]
654fn dispatch_session_list(pre: &GlobalArgs) -> Result<()> {
655 let entries = crate::session::session_list()?;
656 if pre.json_output {
657 let v = serde_json::to_value(&entries)?;
658 output_result(v, &pre.output_options())?;
659 return Ok(());
660 }
661 if entries.is_empty() {
662 println!("No sessions.");
663 return Ok(());
664 }
665 for e in entries {
666 let status = if e.alive { "alive" } else { "dead" };
667 println!(
668 " {:<20} {status:<6} pid={} {} ({})",
669 e.name, e.pid, e.transport, e.source
670 );
671 }
672 Ok(())
673}
674
675#[cfg(unix)]
676fn dispatch_session_start(pre: &GlobalArgs, name: &str) -> Result<()> {
677 let (source, is_stdio) = if let Some(cmd) = &pre.mcp_stdio {
678 if oauth_wanted(pre) {
679 return Err(Error::usage(
680 "OAuth is not supported with --mcp-stdio (HTTP discovery required)",
681 ));
682 }
683 (cmd.clone(), true)
684 } else if let Some(url) = &pre.mcp {
685 (url.clone(), false)
686 } else {
687 return Err(Error::usage(
688 "--session-start requires --mcp or --mcp-stdio",
689 ));
690 };
691
692 let mut auth = pre.parse_auth_headers()?;
693 if !is_stdio && oauth_wanted(pre) {
694 let rt = tokio::runtime::Builder::new_multi_thread()
695 .enable_all()
696 .build()
697 .map_err(|e| Error::runtime(e.to_string()))?;
698 if let Some(o) = rt.block_on(setup_from_args(pre))? {
699 let token = rt.block_on(o.access_token())?;
700 auth.retain(|(k, _)| !k.eq_ignore_ascii_case("authorization"));
701 auth.push(("Authorization".into(), format!("Bearer {token}")));
702 }
703 }
704
705 let idle_secs = crate::session::resolve_idle_secs(pre.session_idle_secs);
706 let config = crate::session::DaemonConfig {
707 name: name.to_string(),
708 source,
709 is_stdio,
710 auth_headers: auth,
711 env_vars: pre.parse_env_vars()?,
712 transport: pre.transport.clone(),
713 clean_env: pre.session_clean_env,
714 idle_secs,
715 };
716 crate::session::session_start(config)
717}
718
719#[cfg(unix)]
720fn dispatch_session(
721 pre: &GlobalArgs,
722 name: &str,
723 remaining: &[String],
724 bake: Option<&BakeConfig>,
725) -> Result<()> {
726 let _ = crate::session::clear_stale_session(name)?;
727 if !crate::session::session_sock_path(name).exists() {
728 return Err(Error::runtime(format!(
729 "session {name:?} is not running. Start with --session-start {name}"
730 )));
731 }
732
733 if pre.list_resources {
734 let v = crate::session::session_request(name, "list_resources", Value::Null)?;
735 output_result(v, &pre.output_options())?;
736 return Ok(());
737 }
738 if pre.list_resource_templates {
739 let v = crate::session::session_request(name, "list_resource_templates", Value::Null)?;
740 output_result(v, &pre.output_options())?;
741 return Ok(());
742 }
743 if let Some(uri) = &pre.read_resource {
744 let v = crate::session::session_request(
745 name,
746 "read_resource",
747 serde_json::json!({ "uri": uri }),
748 )?;
749 output_result(v, &pre.output_options())?;
750 return Ok(());
751 }
752 if pre.list_prompts {
753 let v = crate::session::session_request(name, "list_prompts", Value::Null)?;
754 output_result(v, &pre.output_options())?;
755 return Ok(());
756 }
757 if let Some(pname) = &pre.get_prompt {
758 let mut args = Map::new();
759 for item in &pre.prompt_arg {
760 let Some((k, v)) = item.split_once('=') else {
761 return Err(Error::usage(format!(
762 "invalid --prompt-arg {item:?}; expected KEY=VALUE"
763 )));
764 };
765 args.insert(k.to_string(), Value::String(v.to_string()));
766 }
767 let v = crate::session::session_request(
768 name,
769 "get_prompt",
770 serde_json::json!({ "name": pname, "arguments": args }),
771 )?;
772 output_result(v, &pre.output_options())?;
773 return Ok(());
774 }
775
776 let src_hash = source_hash_for(&format!("session:{name}"));
777 let list_opts = ListOptions::from_global(pre, ListStyle::Mcp);
778 let detail = pre.list_detail();
779 let use_light_index = matches!(detail, ListDetail::Names) && pre.describe.is_none();
780 let needs_catalog = pre.list_commands
781 || pre.describe.is_some()
782 || remaining.is_empty()
783 || remaining.get(1).map(String::as_str) == Some("--help")
784 || remaining.get(1).map(String::as_str) == Some("-h");
785
786 let load_catalog = || -> Result<Vec<crate::CommandDef>> {
787 let tools_val = crate::session::session_request(
788 name,
789 "list_tools",
790 serde_json::json!({ "refresh": pre.refresh }),
791 )?;
792 let tools = tools_val
793 .as_array()
794 .cloned()
795 .ok_or_else(|| Error::runtime("session list_tools did not return an array"))?;
796 Ok(apply_bake_filter(tools_to_commands(&tools), bake))
797 };
798
799 let load_light_catalog = || -> Result<Vec<crate::CommandDef>> {
800 let mut params = serde_json::json!({ "refresh": pre.refresh });
801 if let Some(pat) = &pre.search_pattern {
802 params["search"] = Value::String(pat.clone());
803 }
804 let tools_val = crate::session::session_request(name, "list_tools_light", params)?;
805 let tools = tools_val
806 .as_array()
807 .cloned()
808 .ok_or_else(|| Error::runtime("session list_tools_light did not return an array"))?;
809 Ok(apply_bake_filter(tools_to_light_commands(&tools), bake))
810 };
811
812 if needs_catalog {
813 let mut commands = if use_light_index {
814 load_light_catalog()?
815 } else {
816 load_catalog()?
817 };
818
819 if let Some(dname) = &pre.describe {
820 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
821 return describe_tool(&commands, dname, pre);
822 }
823
824 if pre.list_commands {
825 if use_light_index {
826 if commands.is_empty() {
828 if let Some(pat) = &pre.search_pattern {
829 if pre.json_output || pre.agent {
830 list_commands(&commands, &list_opts)?;
831 } else {
832 println!("\nNo tools matching '{pat}'.");
833 }
834 return Ok(());
835 }
836 } else if let Some(pat) = &pre.search_pattern {
837 if !pre.quiet_list() {
838 println!("\nTools matching '{pat}':");
839 }
840 } else if !pre.quiet_list() {
841 println!("\nAvailable tools:");
842 }
843 } else if let Some(pat) = &pre.search_pattern {
844 commands = filter_by_search(commands, pat);
845 if commands.is_empty() {
846 if pre.json_output || pre.agent {
847 list_commands(&commands, &list_opts)?;
848 } else {
849 println!("\nNo tools matching '{pat}'.");
850 }
851 return Ok(());
852 }
853 if !pre.quiet_list() {
854 println!("\nTools matching '{pat}':");
855 }
856 } else if !pre.quiet_list() {
857 println!("\nAvailable tools:");
858 }
859 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
860 return list_commands(&commands, &list_opts);
861 }
862
863 if remaining.is_empty() {
864 if !pre.quiet_list() {
865 println!("Available tools:");
866 }
867 let commands = apply_list_options(commands, &src_hash, pre.sort.as_deref(), pre.top);
868 list_commands(&commands, &list_opts)?;
869 if !pre.quiet_list() {
870 println!("\nUse --list for the same output, or provide a subcommand.");
871 }
872 return Ok(());
873 }
874
875 if maybe_tool_help(&commands, remaining, pre)? {
876 return Ok(());
877 }
878 }
879
880 let tname = remaining
882 .first()
883 .ok_or_else(|| Error::usage("no subcommand specified"))?;
884 let tool_val = crate::session::session_request(
885 name,
886 "get_tool",
887 serde_json::json!({ "name": tname, "refresh": pre.refresh }),
888 )?;
889 let commands = if tool_val.is_null() {
890 load_catalog()?
891 } else {
892 let one = tools_to_commands(std::slice::from_ref(&tool_val));
893 apply_bake_filter(one, bake)
894 };
895
896 let parsed = match parse_tool_args(&commands, remaining) {
897 Err(Error::Usage(msg)) if msg == "__help__" => return Ok(()),
898 other => other?,
899 };
900
901 let arguments: Map<String, Value> = if parsed.stdin {
902 match read_stdin_json("MCP tool arguments")? {
903 Value::Object(map) => map,
904 other => {
905 return Err(Error::runtime(format!(
906 "MCP --stdin expects a JSON object, got {}",
907 other
908 )));
909 }
910 }
911 } else {
912 parsed.values.into_iter().collect()
913 };
914
915 let tool_name = parsed
916 .command
917 .tool_name
918 .clone()
919 .unwrap_or_else(|| parsed.command.name.clone());
920
921 let data = crate::session::session_request(
922 name,
923 "call_tool",
924 serde_json::json!({
925 "name": tool_name,
926 "arguments": arguments,
927 "full_envelope": pre.full_envelope(),
928 }),
929 )?;
930 output_result(data, &pre.output_options())?;
931 let _ = record_usage(&src_hash, &tool_name);
932 Ok(())
933}
934
935fn print_help() {
936 eprintln!(
937 "\
938skiff {version} — Turn any MCP server, OpenAPI spec, or GraphQL endpoint into a CLI
939
940Usage:
941 skiff --spec <URL|FILE> [--list] [command]
942 skiff --mcp <URL> [--list] [command]
943 skiff --mcp-stdio <CMD> [--list] [command]
944 skiff --graphql <URL> [--list] [command]
945 skiff --mcp-stdio <CMD> --session-start <NAME>
946 skiff --session <NAME> [--list] [command]
947 skiff bake <create|list|show|remove|update|install> ...
948 skiff @<name> ...
949",
950 version = env!("CARGO_PKG_VERSION")
951 );
952}