mcp2cli 3.6.0__tar.gz → 3.7.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp2cli
3
- Version: 3.6.0
3
+ Version: 3.7.0
4
4
  Summary: Turn any MCP server or OpenAPI spec into a CLI
5
5
  Author: Stephan Fitzpatrick
6
6
  Author-email: Stephan Fitzpatrick <stephan@knowsuchagency.com>
@@ -158,6 +158,30 @@ mcp2cli --mcp-stdio "node server.js" --env API_KEY=sk-... --env DEBUG=1 \
158
158
  search --query "test"
159
159
  ```
160
160
 
161
+ ### MCP roots and completion
162
+
163
+ Expose one or more filesystem roots when a server scopes operations to a
164
+ workspace. Paths are converted to `file://` URIs; explicit roots must also use
165
+ the `file://` scheme.
166
+
167
+ ```bash
168
+ mcp2cli --mcp-stdio "npx @modelcontextprotocol/server-filesystem /tmp" \
169
+ --root "$PWD" --root file:///var/shared --list
170
+ ```
171
+
172
+ Request prompt-argument or resource-template completions with
173
+ `REF:ARG=PREFIX`:
174
+
175
+ ```bash
176
+ mcp2cli --mcp https://example.com/mcp \
177
+ --complete "greeting:name=San"
178
+ mcp2cli --mcp https://example.com/mcp \
179
+ --complete "file:///docs/{topic}:topic=api"
180
+ ```
181
+
182
+ Both options work when starting a persistent session; roots are retained by
183
+ the session daemon and completion requests can be sent through `--session`.
184
+
161
185
  ### OpenAPI mode
162
186
 
163
187
  ```bash
@@ -338,6 +362,8 @@ Options:
338
362
  --base-url URL Override base URL from spec
339
363
  --transport TYPE MCP HTTP transport: auto|sse|streamable (default: auto)
340
364
  --env KEY=VALUE Env var for MCP stdio server (repeatable)
365
+ --root PATH|FILE_URI Expose a filesystem root to an MCP server (repeatable)
366
+ --complete SPEC Complete an MCP prompt or resource-template argument
341
367
  --oauth Enable OAuth (authorization code + PKCE flow)
342
368
  --oauth-client-id ID OAuth client ID (supports env:/file: prefixes)
343
369
  --oauth-client-secret S OAuth client secret (supports env:/file: prefixes)
@@ -139,6 +139,30 @@ mcp2cli --mcp-stdio "node server.js" --env API_KEY=sk-... --env DEBUG=1 \
139
139
  search --query "test"
140
140
  ```
141
141
 
142
+ ### MCP roots and completion
143
+
144
+ Expose one or more filesystem roots when a server scopes operations to a
145
+ workspace. Paths are converted to `file://` URIs; explicit roots must also use
146
+ the `file://` scheme.
147
+
148
+ ```bash
149
+ mcp2cli --mcp-stdio "npx @modelcontextprotocol/server-filesystem /tmp" \
150
+ --root "$PWD" --root file:///var/shared --list
151
+ ```
152
+
153
+ Request prompt-argument or resource-template completions with
154
+ `REF:ARG=PREFIX`:
155
+
156
+ ```bash
157
+ mcp2cli --mcp https://example.com/mcp \
158
+ --complete "greeting:name=San"
159
+ mcp2cli --mcp https://example.com/mcp \
160
+ --complete "file:///docs/{topic}:topic=api"
161
+ ```
162
+
163
+ Both options work when starting a persistent session; roots are retained by
164
+ the session daemon and completion requests can be sent through `--session`.
165
+
142
166
  ### OpenAPI mode
143
167
 
144
168
  ```bash
@@ -319,6 +343,8 @@ Options:
319
343
  --base-url URL Override base URL from spec
320
344
  --transport TYPE MCP HTTP transport: auto|sse|streamable (default: auto)
321
345
  --env KEY=VALUE Env var for MCP stdio server (repeatable)
346
+ --root PATH|FILE_URI Expose a filesystem root to an MCP server (repeatable)
347
+ --complete SPEC Complete an MCP prompt or resource-template argument
322
348
  --oauth Enable OAuth (authorization code + PKCE flow)
323
349
  --oauth-client-id ID OAuth client ID (supports env:/file: prefixes)
324
350
  --oauth-client-secret S OAuth client secret (supports env:/file: prefixes)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp2cli"
3
- version = "3.6.0"
3
+ version = "3.7.0"
4
4
  description = "Turn any MCP server or OpenAPI spec into a CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp2cli"
3
- version = "3.6.0"
3
+ version = "3.7.0"
4
4
  description = "Turn any MCP server or OpenAPI spec into a CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -38,6 +38,8 @@ CACHE_DIR = Path(
38
38
  os.environ.get("MCP2CLI_CACHE_DIR", Path.home() / ".cache" / "mcp2cli")
39
39
  )
40
40
  DEFAULT_CACHE_TTL = 3600
41
+ # Client-side capability the user opted into (see --root).
42
+ _ROOTS: list[str] = []
41
43
  USAGE_FILE = CACHE_DIR / "usage.json"
42
44
  CONFIG_DIR = Path(
43
45
  os.environ.get("MCP2CLI_CONFIG_DIR", Path.home() / ".config" / "mcp2cli")
@@ -322,6 +324,7 @@ _MCP_RENAMED_FIELDS = {
322
324
  "mimeType": "mime_type",
323
325
  "structuredContent": "structured_content",
324
326
  "isError": "is_error",
327
+ "hasMore": "has_more",
325
328
  }
326
329
 
327
330
 
@@ -2884,6 +2887,46 @@ def _run_mcp_clean(fn, source: str):
2884
2887
  sys.exit(1)
2885
2888
 
2886
2889
 
2890
+ def _normalize_root(raw: str) -> str:
2891
+ """Return a validated file URI for one ``--root`` value."""
2892
+ if "://" in raw:
2893
+ if not raw.casefold().startswith("file://"):
2894
+ raise ValueError(
2895
+ f"--root expects a filesystem path or file:// URI, got {raw!r}"
2896
+ )
2897
+ uri = raw
2898
+ else:
2899
+ uri = Path(raw).expanduser().resolve().as_uri()
2900
+
2901
+ from mcp import types
2902
+
2903
+ try:
2904
+ return str(types.Root(uri=uri).uri)
2905
+ except (TypeError, ValueError) as exc:
2906
+ raise ValueError(f"invalid --root value {raw!r}: {exc}") from exc
2907
+
2908
+
2909
+ def _roots_callback(root_uris: list[str] | None = None):
2910
+ """Answer ``roots/list`` from validated file URIs, if any were given."""
2911
+ configured_roots = tuple(_ROOTS if root_uris is None else root_uris)
2912
+ if not configured_roots:
2913
+ return None
2914
+
2915
+ from mcp import types
2916
+
2917
+ async def list_roots(context=None):
2918
+ roots = [
2919
+ types.Root(
2920
+ uri=uri,
2921
+ name=Path(urlparse(uri).path).name or uri,
2922
+ )
2923
+ for uri in configured_roots
2924
+ ]
2925
+ return types.ListRootsResult(roots=roots)
2926
+
2927
+ return list_roots
2928
+
2929
+
2887
2930
  def run_mcp_http(
2888
2931
  url: str,
2889
2932
  auth_headers: list[tuple[str, str]],
@@ -2903,6 +2946,7 @@ def run_mcp_http(
2903
2946
  prompt_action: str | None = None,
2904
2947
  prompt_name: str | None = None,
2905
2948
  prompt_arguments: dict | None = None,
2949
+ complete_spec: str | None = None,
2906
2950
  search_pattern: str | None = None,
2907
2951
  head: int | None = None,
2908
2952
  verbose: bool = False,
@@ -2918,6 +2962,7 @@ def run_mcp_http(
2918
2962
  prompt_action=prompt_action,
2919
2963
  prompt_name=prompt_name,
2920
2964
  prompt_arguments=prompt_arguments,
2965
+ complete_spec=complete_spec,
2921
2966
  search_pattern=search_pattern,
2922
2967
  head=head,
2923
2968
  verbose=verbose,
@@ -2937,7 +2982,7 @@ def run_mcp_http(
2937
2982
  async with _streamable_streams(
2938
2983
  url, headers=headers, auth=oauth_provider
2939
2984
  ) as (read, write):
2940
- async with ClientSession(read, write) as session:
2985
+ async with ClientSession(read, write, list_roots_callback=_roots_callback()) as session:
2941
2986
  await session.initialize()
2942
2987
  return await _mcp_session(
2943
2988
  session,
@@ -2960,7 +3005,7 @@ def run_mcp_http(
2960
3005
  read,
2961
3006
  write,
2962
3007
  ):
2963
- async with ClientSession(read, write) as session:
3008
+ async with ClientSession(read, write, list_roots_callback=_roots_callback()) as session:
2964
3009
  await session.initialize()
2965
3010
  return await _mcp_session(
2966
3011
  session,
@@ -3008,6 +3053,7 @@ def run_mcp_stdio(
3008
3053
  prompt_action: str | None = None,
3009
3054
  prompt_name: str | None = None,
3010
3055
  prompt_arguments: dict | None = None,
3056
+ complete_spec: str | None = None,
3011
3057
  search_pattern: str | None = None,
3012
3058
  head: int | None = None,
3013
3059
  verbose: bool = False,
@@ -3023,6 +3069,7 @@ def run_mcp_stdio(
3023
3069
  prompt_action=prompt_action,
3024
3070
  prompt_name=prompt_name,
3025
3071
  prompt_arguments=prompt_arguments,
3072
+ complete_spec=complete_spec,
3026
3073
  search_pattern=search_pattern,
3027
3074
  head=head,
3028
3075
  verbose=verbose,
@@ -3044,7 +3091,7 @@ def run_mcp_stdio(
3044
3091
  params = StdioServerParameters(command=parts[0], args=parts[1:], env=env)
3045
3092
 
3046
3093
  async with stdio_client(params) as (read, write):
3047
- async with ClientSession(read, write) as session:
3094
+ async with ClientSession(read, write, list_roots_callback=_roots_callback()) as session:
3048
3095
  await session.initialize()
3049
3096
  return await _mcp_session(
3050
3097
  session,
@@ -3081,6 +3128,7 @@ async def _mcp_session(
3081
3128
  prompt_action: str | None = None,
3082
3129
  prompt_name: str | None = None,
3083
3130
  prompt_arguments: dict | None = None,
3131
+ complete_spec: str | None = None,
3084
3132
  search_pattern: str | None = None,
3085
3133
  head: int | None = None,
3086
3134
  verbose: bool = False,
@@ -3090,6 +3138,14 @@ async def _mcp_session(
3090
3138
  source_hash: str = "",
3091
3139
  json_output: bool = False,
3092
3140
  ):
3141
+ # Handle completion requests
3142
+ if complete_spec:
3143
+ await _handle_completion(
3144
+ session, complete_spec, pretty, raw, toon, head=head,
3145
+ json_output=json_output,
3146
+ )
3147
+ return
3148
+
3093
3149
  # Handle resource operations
3094
3150
  if resource_action:
3095
3151
  await _handle_resources(
@@ -3232,6 +3288,67 @@ async def _handle_resources(
3232
3288
  # ---------------------------------------------------------------------------
3233
3289
 
3234
3290
 
3291
+ def parse_complete_spec(spec: str) -> tuple[str, str, str]:
3292
+ """Parse ``REF:ARG=PREFIX`` into (ref, argument, prefix).
3293
+
3294
+ ``REF`` is a prompt name, or a resource URI template when it contains
3295
+ ``://`` — in which case the scheme's own colons must not be mistaken for
3296
+ the ref/arg separator.
3297
+ """
3298
+ ref_part, sep, prefix = spec.partition("=")
3299
+ if not sep:
3300
+ print(
3301
+ "Error: --complete expects REF:ARG=PREFIX (e.g. 'my-prompt:city=San')",
3302
+ file=sys.stderr,
3303
+ )
3304
+ sys.exit(1)
3305
+ ref, sep, argument = ref_part.rpartition(":")
3306
+ if not sep or not ref or not argument:
3307
+ print(
3308
+ f"Error: --complete could not split {ref_part!r} into REF:ARG",
3309
+ file=sys.stderr,
3310
+ )
3311
+ sys.exit(1)
3312
+ return ref, argument, prefix
3313
+
3314
+
3315
+ async def _completion_data(session, spec: str) -> dict:
3316
+ """Run ``completion/complete`` and return its stable wire-shaped payload."""
3317
+ from mcp import types
3318
+
3319
+ ref_name, argument, prefix = parse_complete_spec(spec)
3320
+ if "://" in ref_name or "{" in ref_name:
3321
+ ref = types.ResourceTemplateReference(
3322
+ type="ref/resource", uri=ref_name
3323
+ )
3324
+ else:
3325
+ ref = types.PromptReference(type="ref/prompt", name=ref_name)
3326
+
3327
+ result = await session.complete(ref, {"name": argument, "value": prefix})
3328
+ completion = result.completion
3329
+ return {
3330
+ "values": list(completion.values or []),
3331
+ "total": completion.total,
3332
+ "hasMore": _mcp_attr(completion, "hasMore"),
3333
+ }
3334
+
3335
+
3336
+ async def _handle_completion(
3337
+ session,
3338
+ spec: str,
3339
+ pretty: bool,
3340
+ raw: bool,
3341
+ toon: bool,
3342
+ head: int | None = None,
3343
+ json_output: bool = False,
3344
+ ):
3345
+ """Run ``completion/complete`` for a prompt or resource-template argument."""
3346
+ data = await _completion_data(session, spec)
3347
+ output_result(
3348
+ data, pretty=pretty, raw=raw, toon=toon, head=head, json_output=json_output
3349
+ )
3350
+
3351
+
3235
3352
  async def _handle_prompts(
3236
3353
  session,
3237
3354
  action: str,
@@ -3354,6 +3471,7 @@ def session_start(
3354
3471
  auth_headers: list[tuple[str, str]],
3355
3472
  env_vars: dict[str, str],
3356
3473
  transport: str = "auto",
3474
+ roots: list[str] | None = None,
3357
3475
  ):
3358
3476
  """Start a persistent session daemon."""
3359
3477
  SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
@@ -3384,6 +3502,7 @@ def session_start(
3384
3502
  "auth_headers": auth_headers,
3385
3503
  "env_vars": env_vars,
3386
3504
  "transport": transport,
3505
+ "roots": list(roots or []),
3387
3506
  }
3388
3507
  )
3389
3508
 
@@ -3536,6 +3655,10 @@ async def _dispatch_get_prompt(session, params):
3536
3655
  return {"description": result.description or "", "messages": messages}
3537
3656
 
3538
3657
 
3658
+ async def _dispatch_complete(session, params):
3659
+ return await _completion_data(session, params["spec"])
3660
+
3661
+
3539
3662
  _SESSION_DISPATCH = {
3540
3663
  "list_tools": _dispatch_list_tools,
3541
3664
  "call_tool": _dispatch_call_tool,
@@ -3544,6 +3667,7 @@ _SESSION_DISPATCH = {
3544
3667
  "list_resource_templates": _dispatch_list_resource_templates,
3545
3668
  "list_prompts": _dispatch_list_prompts,
3546
3669
  "get_prompt": _dispatch_get_prompt,
3670
+ "complete": _dispatch_complete,
3547
3671
  }
3548
3672
 
3549
3673
 
@@ -3556,6 +3680,7 @@ def _run_session_daemon(config_json: str):
3556
3680
  auth_headers = [tuple(h) for h in config["auth_headers"]]
3557
3681
  env_vars = config["env_vars"]
3558
3682
  transport = config["transport"]
3683
+ roots = config.get("roots", [])
3559
3684
 
3560
3685
  sock_path = _session_sock_path(name)
3561
3686
  meta_path = _session_meta_path(name)
@@ -3676,7 +3801,7 @@ def _run_session_daemon(config_json: str):
3676
3801
  env = {**os.environ, **env_vars}
3677
3802
  params = StdioServerParameters(command=parts[0], args=parts[1:], env=env)
3678
3803
  async with stdio_client(params) as (read, write):
3679
- async with ClientSession(read, write) as session:
3804
+ async with ClientSession(read, write, list_roots_callback=_roots_callback(roots)) as session:
3680
3805
  await _run_with_session(session)
3681
3806
  else:
3682
3807
  headers = dict(auth_headers) if auth_headers else None
@@ -3686,14 +3811,14 @@ def _run_session_daemon(config_json: str):
3686
3811
  read,
3687
3812
  write,
3688
3813
  ):
3689
- async with ClientSession(read, write) as session:
3814
+ async with ClientSession(read, write, list_roots_callback=_roots_callback(roots)) as session:
3690
3815
  await _run_with_session(session)
3691
3816
 
3692
3817
  async def _via_sse():
3693
3818
  from mcp.client.sse import sse_client
3694
3819
 
3695
3820
  async with sse_client(source, headers=headers) as (read, write):
3696
- async with ClientSession(read, write) as session:
3821
+ async with ClientSession(read, write, list_roots_callback=_roots_callback(roots)) as session:
3697
3822
  await _run_with_session(session)
3698
3823
 
3699
3824
  if transport == "sse":
@@ -3821,6 +3946,7 @@ def handle_mcp(
3821
3946
  prompt_action: str | None = None,
3822
3947
  prompt_name: str | None = None,
3823
3948
  prompt_arguments: dict | None = None,
3949
+ complete_spec: str | None = None,
3824
3950
  search_pattern: str | None = None,
3825
3951
  bake_config: BakeConfig | None = None,
3826
3952
  head: int | None = None,
@@ -3842,14 +3968,15 @@ def handle_mcp(
3842
3968
  key = cache_key_override or cache_key_for(config_for_cache)
3843
3969
  src_hash = _source_hash_for(source)
3844
3970
 
3845
- # Resource/prompt operations skip the tool flow entirely
3846
- if resource_action or prompt_action:
3971
+ # Resource/prompt/completion operations skip the tool flow entirely
3972
+ if resource_action or prompt_action or complete_spec:
3847
3973
  extra = dict(
3848
3974
  resource_action=resource_action,
3849
3975
  resource_uri=resource_uri,
3850
3976
  prompt_action=prompt_action,
3851
3977
  prompt_name=prompt_name,
3852
3978
  prompt_arguments=prompt_arguments,
3979
+ complete_spec=complete_spec,
3853
3980
  head=head,
3854
3981
  json_output=json_output,
3855
3982
  )
@@ -3977,7 +4104,7 @@ def _fetch_mcp_tools(
3977
4104
  env = {**os.environ, **env_vars}
3978
4105
  params = StdioServerParameters(command=parts[0], args=parts[1:], env=env)
3979
4106
  async with stdio_client(params) as (read, write):
3980
- async with ClientSession(read, write) as session:
4107
+ async with ClientSession(read, write, list_roots_callback=_roots_callback()) as session:
3981
4108
  await session.initialize()
3982
4109
  await _extract_tools(session)
3983
4110
  else:
@@ -3989,7 +4116,7 @@ def _fetch_mcp_tools(
3989
4116
  async with _streamable_streams(
3990
4117
  source, headers=headers, auth=oauth_provider
3991
4118
  ) as (read, write):
3992
- async with ClientSession(read, write) as session:
4119
+ async with ClientSession(read, write, list_roots_callback=_roots_callback()) as session:
3993
4120
  await session.initialize()
3994
4121
  await _extract_tools(session)
3995
4122
 
@@ -4000,7 +4127,7 @@ def _fetch_mcp_tools(
4000
4127
  read,
4001
4128
  write,
4002
4129
  ):
4003
- async with ClientSession(read, write) as session:
4130
+ async with ClientSession(read, write, list_roots_callback=_roots_callback()) as session:
4004
4131
  await session.initialize()
4005
4132
  await _extract_tools(session)
4006
4133
 
@@ -4199,6 +4326,26 @@ def _build_main_parser() -> argparse.ArgumentParser:
4199
4326
  default=[],
4200
4327
  help="Environment variable KEY=VALUE for MCP stdio (repeatable)",
4201
4328
  )
4329
+ pre.add_argument(
4330
+ "--root",
4331
+ action="append",
4332
+ default=[],
4333
+ metavar="PATH|FILE_URI",
4334
+ help=(
4335
+ "Expose a filesystem path or file:// URI to the server (repeatable). "
4336
+ "Workspace-scoped servers request these via roots/list."
4337
+ ),
4338
+ )
4339
+ pre.add_argument(
4340
+ "--complete",
4341
+ default=None,
4342
+ metavar="REF:ARG=PREFIX",
4343
+ help=(
4344
+ "Ask the server to complete an argument value, e.g. "
4345
+ "--complete 'my-prompt:city=San'. REF is a prompt name, or a "
4346
+ "resource URI template when it contains '://'."
4347
+ ),
4348
+ )
4202
4349
  pre.add_argument(
4203
4350
  "--oauth",
4204
4351
  action="store_true",
@@ -4426,6 +4573,7 @@ def _handle_session_operations(
4426
4573
  auth_headers,
4427
4574
  env_vars,
4428
4575
  transport=pre_args.transport,
4576
+ roots=_ROOTS,
4429
4577
  )
4430
4578
  return True
4431
4579
 
@@ -4438,6 +4586,13 @@ def _handle_session_operations(
4438
4586
  pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
4439
4587
  json_output=pre_args.json_output,
4440
4588
  )
4589
+ if pre_args.complete:
4590
+ result = _session_request(
4591
+ sess_name, "complete", {"spec": pre_args.complete}
4592
+ )
4593
+ output_result(result, head=pre_args.head, **_sess_out)
4594
+ return True
4595
+
4441
4596
 
4442
4597
  if pre_args.list_resources:
4443
4598
  result = _session_request(sess_name, "list_resources")
@@ -4693,6 +4848,12 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
4693
4848
  pre_args, leftover = pre.parse_known_args(global_argv)
4694
4849
  remaining = leftover + tool_argv
4695
4850
 
4851
+ try:
4852
+ _ROOTS[:] = [_normalize_root(raw) for raw in pre_args.root]
4853
+ except ValueError as exc:
4854
+ print(f"Error: {exc}", file=sys.stderr)
4855
+ sys.exit(1)
4856
+
4696
4857
  # --search implies --list
4697
4858
  search_pattern = pre_args.search_pattern
4698
4859
  if search_pattern:
@@ -4764,6 +4925,7 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
4764
4925
  prompt_action=prompt_action,
4765
4926
  prompt_name=prompt_name,
4766
4927
  prompt_arguments=prompt_arguments,
4928
+ complete_spec=pre_args.complete,
4767
4929
  search_pattern=search_pattern,
4768
4930
  bake_config=bake_config,
4769
4931
  head=pre_args.head,
File without changes
File without changes