1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
---
name: mcporter
description: Use when calling MCP tools via the mcporter CLI — covers call syntax, argument formats, timestamp handling, detecting errors, and tool discovery.
---

# mcporter

CLI for calling MCP server tools. Run commands via Bash.

## Syntax

```bash
mcporter call <server>.<tool> key=value key2=value2
mcporter call <server>.<tool> --args '{"key":"value"}'
```

The server and tool are joined with a dot — `<server>.<tool>`, not two
space-delimited words.

```bash
# Wrong — space instead of a dot between server and tool.
mcporter call slack slack_send_message channel_id=C123 message=hi

# Right.
mcporter call slack.slack_send_message channel_id=C123 message=hi
```

Both `key=value` and `--args` JSON work. Prefer `key=value` for simple
calls. Use `--args` JSON when any parameter is a string that looks
numeric — `key=value` coerces values, which breaks schema validation
for string-typed fields like timestamps.

```bash
# key=value coerces 1772034831.268949 to a number — fails validation.
mcporter call slack.get_thread_messages channel=general thread_ts=1772034831.268949

# --args preserves the string type.
mcporter call slack.get_thread_messages --args '{"channel":"general","thread_ts":"1772034831.268949"}'
```

## Errors

`mcporter call` exits 0 when the upstream server returns an error, so a
script can't trust `$?`. The failure shows up in the payload instead —
an `APIResponseError` with `code` and `status` keys, and the readable
message nested in `body` as a JSON string:

```bash
resp=$(mcporter call notion.notion-query-data-sources --args "$args")
if [[ $(jq -r 'has("code") and has("status")' <<<"$resp") == true ]]; then
    jq -r '(try (.body | fromjson | .message)) // .code' <<<"$resp" >&2
    exit 1
fi
```

Write calls can also outrun the default 60 s timeout — pass
`--timeout 120000` or set `MCPORTER_CALL_TIMEOUT=120000`.

## Config discovery

`--config` defaults to `./config/mcporter.json` — relative to the current
directory, and never searched for in a parent. A project config there adds its
servers to the global `~/.mcporter/mcporter.json` set, so the same command sees
a different set of servers depending on where you run it.

A broken project config takes down the whole load, global servers included, and
every mcporter-backed skill with it:

```
[mcporter] Invalid URL
    at normalizeServerEntry (.../config-normalize.js:20:18)
```

Which fields get env-expanded is version-dependent. 0.8.1 expands only `headers`
and `env`, so a `${VAR}` in `baseUrl` reaches `new URL()` verbatim and throws
whether or not the variable is set; 0.13.4 expands every field *except* those
two, and honors `${VAR:-default}` fallbacks. Assume nothing in between.

`MCPORTER_CONFIG=<path>` replaces the config layers rather than adding to them,
so a project config belongs off `config/mcporter.json` and named explicitly —
that keeps it out of unrelated runs on every version. Failing that, run from a
subdirectory to skip it: `(cd app && mcporter list)`.

## Discovering tools

```bash
mcporter list                    # List configured servers
mcporter list <server> --schema  # Show all tools with full parameter schemas
mcporter call --help             # Full call syntax
```