diff --git a/.gitattributes b/.gitattributes
index d0c0c4c..85916ad 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,5 @@
* text eol=lf
*.png binary
+*.tar.gz binary
+*.gz binary
+*.tgz binary
diff --git a/README.md b/README.md
index be82bb6..c061625 100644
--- a/README.md
+++ b/README.md
@@ -42,11 +42,16 @@ npx mcporter call 'linear.create_comment(issueId: "ENG-123", body: "Looks good!"
```bash
npx mcporter list
npx mcporter list context7 --schema
+npx mcporter list context7 --brief # Show concise signatures
+npx mcporter list context7 "resolve-*" --brief # Filter tools by name pattern
+npx mcporter list context7 query-docs --brief # Check for a specific tool
npx mcporter list https://mcp.linear.app/mcp --all-parameters
npx mcporter list shadcn.io/api/mcp.getComponents # URL + tool suffix auto-resolves
npx mcporter list --stdio "bun run ./local-server.ts" --env TOKEN=xyz
```
+- Add `--brief` to show concise TypeScript-style function signatures instead of full details. This mode supports filtering tools by name using glob patterns (e.g. `list server "amz_*"`).
+
- Add `--json` to emit a machine-readable summary with per-server statuses (auth/offline/http/error counts) and, for single-server runs, the full tool schema payload.
- Add `--verbose` to show every config source that registered the server name (primary first), both in text and JSON list output.
@@ -149,11 +154,12 @@ Helpful flags:
- `--save-images
` (on `mcporter call`) -- save MCP image content blocks to files in the given directory (opt-in; stdout output shape stays unchanged).
- `--raw-strings` (on `mcporter call`) -- keep numeric-looking argument values (for `key=value`, `key:value`, and trailing positional values) as strings.
- `--no-coerce` (on `mcporter call`) -- keep all `key=value` and positional values as raw strings (disables bool/null/number/JSON coercion).
-- `--json` (on `mcporter list`) -- emit JSON summaries/counts instead of text. Multi-server runs report per-server statuses, counts, and connection issues; single-server runs include the full tool metadata.
+- `--json` (on `mcporter list`) -- emit JSON summaries/counts instead of text. Multi-server runs report per-server statuses, counts, and connection issues; single-server runs include the full tool metadata. Mutually exclusive with `--brief`.
- `--output json/raw` (on `mcporter call`) -- when a connection fails, MCPorter prints the usual colorized hint and also emits a structured `{ server, tool, issue }` envelope so scripts can handle auth/offline/http errors programmatically.
- `--json` (on `mcporter auth`) -- emit the same structured connection envelope whenever OAuth/transport setup fails, instead of throwing an error.
- `--json` (on `mcporter emit-ts`) -- print a JSON summary describing the emitted files (mode + output paths) instead of text logs—handy when generating artifacts inside scripts.
- `--all-parameters` -- show every schema field when listing a server (default output shows at least five parameters plus a summary of the rest).
+- `--brief` -- show concise tool signatures only; supports filtering tools by name (e.g. `mcporter list server "query_*" --brief`). Mutually exclusive with `--json`, `--schema`, `--verbose`, and `--all-parameters`.
- `--http-url ` / `--stdio "command …"` -- describe an ad-hoc MCP server inline. STDIO transports now inherit your current shell environment automatically; add `--env KEY=value` only when you need to inject/override variables alongside `--cwd`, `--name`, or `--persist `. These flags now work with `mcporter auth` too, so `mcporter auth https://mcp.example.com/mcp` just works.
- For OAuth-protected servers such as `vercel`, run `npx mcporter auth vercel` once to complete login.
diff --git a/docs/cli-reference.md b/docs/cli-reference.md
index 155200a..80d5c79 100644
--- a/docs/cli-reference.md
+++ b/docs/cli-reference.md
@@ -19,6 +19,8 @@ A quick reference for the primary `mcporter` subcommands. Each command inherits
- Flags:
- `--all-parameters` – include every optional parameter in the signature.
- `--schema` – pretty-print the JSON schema for each tool.
+ - `--brief` – show concise tool signatures only. Mutually exclusive with `--json`, `--schema`, etc.
+ - `--json` – emit a machine-readable summary instead of text. Mutually exclusive with `--brief`.
- `--timeout ` – per-server timeout when enumerating all servers.
## `mcporter call `
diff --git a/package.json b/package.json
index ef57152..0cf37c0 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"commander": "^14.0.3",
"es-toolkit": "^1.45.0",
"jsonc-parser": "^3.3.1",
+ "minimatch": "^10.1.2",
"ora": "^9.3.0",
"rolldown": "1.0.0-rc.6",
"zod": "^4.3.6"
@@ -101,4 +102,4 @@
}
]
}
-}
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 01913da..6c1506a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
jsonc-parser:
specifier: ^3.3.1
version: 3.3.1
+ minimatch:
+ specifier: ^10.1.2
+ version: 10.1.2
ora:
specifier: ^9.3.0
version: 9.3.0
@@ -338,6 +341,14 @@ packages:
'@iarna/toml@2.2.5':
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
+ '@isaacs/balanced-match@4.0.1':
+ resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
+ engines: {node: 20 || >=22}
+
+ '@isaacs/brace-expansion@5.0.1':
+ resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
+ engines: {node: 20 || >=22}
+
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -1200,6 +1211,10 @@ packages:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
+ minimatch@10.1.2:
+ resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
+ engines: {node: 20 || >=22}
+
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
@@ -1722,6 +1737,12 @@ snapshots:
'@iarna/toml@2.2.5': {}
+ '@isaacs/balanced-match@4.0.1': {}
+
+ '@isaacs/brace-expansion@5.0.1':
+ dependencies:
+ '@isaacs/balanced-match': 4.0.1
+
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
@@ -2463,6 +2484,10 @@ snapshots:
mimic-function@5.0.1: {}
+ minimatch@10.1.2:
+ dependencies:
+ '@isaacs/brace-expansion': 5.0.1
+
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.4
diff --git a/src/cli/list-command.ts b/src/cli/list-command.ts
index 952d482..7e71449 100644
--- a/src/cli/list-command.ts
+++ b/src/cli/list-command.ts
@@ -8,6 +8,7 @@ import { prepareEphemeralServerTarget } from './ephemeral-target.js';
import { splitHttpToolSelector } from './http-utils.js';
import { chooseClosestIdentifier, renderIdentifierResolutionMessages } from './identifier-helpers.js';
import { formatExampleBlock } from './list-detail-helpers.js';
+import { filterToolsByPattern } from './list-filter.js';
import type { ListSummaryResult, StatusCategory } from './list-format.js';
import { classifyListError, formatSourceSuffix, renderServerListRow } from './list-format.js';
import {
@@ -16,6 +17,7 @@ import {
createEmptyStatusCounts,
createUnknownResult,
type ListJsonServerEntry,
+ printBriefToolList,
printSingleServerHeader,
printToolDetail,
summarizeStatusCounts,
@@ -34,12 +36,14 @@ export function extractListFlags(args: string[]): {
format: ListOutputFormat;
verbose: boolean;
includeSources: boolean;
+ brief: boolean;
} {
let schema = false;
let timeoutMs: number | undefined;
let requiredOnly = true;
let verbose = false;
let includeSources = false;
+ let brief = false;
const format = consumeOutputFormat(args, {
defaultFormat: 'text',
allowed: ['text', 'json'],
@@ -74,13 +78,39 @@ export function extractListFlags(args: string[]): {
args.splice(index, 1);
continue;
}
+ if (token === '--brief') {
+ brief = true;
+ args.splice(index, 1);
+ continue;
+ }
if (token === '--timeout') {
timeoutMs = consumeTimeoutFlag(args, index, { flagName: '--timeout' });
continue;
}
index += 1;
}
- return { schema, timeoutMs, requiredOnly, ephemeral, format, verbose, includeSources };
+
+ // Validate mutual exclusion for --brief
+ if (brief) {
+ const conflicts: string[] = [];
+ if (format === 'json') {
+ conflicts.push('--json');
+ }
+ if (schema) {
+ conflicts.push('--schema');
+ }
+ if (verbose) {
+ conflicts.push('--verbose');
+ }
+ if (!requiredOnly) {
+ conflicts.push('--all-parameters');
+ }
+ if (conflicts.length > 0) {
+ throw new Error(`--brief cannot be used with ${conflicts.join(', ')}`);
+ }
+ }
+
+ return { schema, timeoutMs, requiredOnly, ephemeral, format, verbose, includeSources, brief };
}
type ListOutputFormat = 'text' | 'json';
@@ -90,7 +120,24 @@ export async function handleList(
args: string[]
): Promise {
const flags = extractListFlags(args);
- let target = args.shift();
+ let target: string | undefined;
+ let toolPattern: string | undefined;
+
+ // In ephemeral mode (e.g. --http-url), positional args are treated as the tool pattern.
+ // In normal mode, the first arg is the target (name/URL), second is the pattern.
+ if (flags.ephemeral) {
+ if (args.length > 0 && args[0] !== undefined && !args[0].startsWith('-')) {
+ toolPattern = args.shift();
+ }
+ } else {
+ // Standard mode: [target] [pattern]
+ if (args.length > 0 && args[0] !== undefined && !args[0].startsWith('-')) {
+ target = args.shift();
+ }
+ if (args.length > 0 && args[0] !== undefined && !args[0].startsWith('-')) {
+ toolPattern = args.shift();
+ }
+ }
if (target) {
const split = splitHttpToolSelector(target);
@@ -297,8 +344,34 @@ export async function handleList(
}
try {
// Always request schemas so we can render CLI-style parameter hints without re-querying per tool.
- const metadataEntries = await withTimeout(loadToolMetadata(runtime, target, { includeSchema: true }), timeoutMs);
+ let metadataEntries = await withTimeout(loadToolMetadata(runtime, target, { includeSchema: true }), timeoutMs);
const durationMs = Date.now() - startedAt;
+
+ // Apply tool pattern filter if specified
+ if (toolPattern) {
+ const filteredNames = new Set(
+ filterToolsByPattern(
+ metadataEntries.map((entry) => ({ name: entry.tool.name })),
+ toolPattern
+ ).map((f) => f.name)
+ );
+ metadataEntries = metadataEntries.filter((e) => filteredNames.has(e.tool.name));
+ }
+
+ // Handle --brief output mode
+ if (flags.brief) {
+ if (metadataEntries.length === 0) {
+ if (toolPattern) {
+ console.log(`No tools matching pattern '${toolPattern}'.`);
+ } else {
+ console.log('No tools available.');
+ }
+ return;
+ }
+ printBriefToolList(metadataEntries, { colorize: true });
+ return;
+ }
+
const summaryLine = printSingleServerHeader(
definition,
metadataEntries.length,
@@ -310,7 +383,11 @@ export async function handleList(
}
);
if (metadataEntries.length === 0) {
- console.log(' Tools: ');
+ if (toolPattern) {
+ console.log(` Tools: `);
+ } else {
+ console.log(' Tools: ');
+ }
console.log(summaryLine);
console.log('');
return;
@@ -354,7 +431,12 @@ export async function handleList(
export function printListHelp(): void {
const lines = [
- 'Usage: mcporter list [server | url] [flags]',
+ 'Usage: mcporter list [target] [tool-pattern] [flags]',
+ '',
+ 'Arguments:',
+ ' Server name from config or URL. (Optional if using ad-hoc flags)',
+ " Filter tools by name (supports glob: 'amz_*').",
+ ' If using ad-hoc flags (e.g. --http-url), the first argument is the pattern.',
'',
'Targets:',
' Use a server from config/mcporter.json or editor imports.',
@@ -373,6 +455,8 @@ export function printListHelp(): void {
' --yes Skip confirmation prompts when persisting.',
'',
'Display flags:',
+ ' --brief Show tools as function signatures only.',
+ ' Cannot be used with --json, --schema, --verbose, --all-parameters.',
' --schema Show tool schemas when listing servers.',
' --all-parameters Include optional parameters in tool docs.',
' --json Emit a JSON summary instead of text.',
@@ -383,6 +467,9 @@ export function printListHelp(): void {
'Examples:',
' mcporter list',
' mcporter list linear --schema',
+ ' mcporter list linear --brief',
+ " mcporter list linear 'add_*'",
+ ' mcporter list linear add_user --schema',
' mcporter list https://mcp.example.com/mcp',
' mcporter list --http-url https://localhost:3333/mcp --schema',
];
diff --git a/src/cli/list-filter.ts b/src/cli/list-filter.ts
new file mode 100644
index 0000000..78e9a8e
--- /dev/null
+++ b/src/cli/list-filter.ts
@@ -0,0 +1,18 @@
+import { minimatch } from 'minimatch';
+
+/**
+ * Filter tools by a glob pattern matching tool names.
+ * @param tools Array of items with a `name` property
+ * @param pattern Glob pattern to match against tool names
+ * @returns Filtered array of tools matching the pattern
+ */
+export function filterToolsByPattern(tools: T[], pattern: string): T[] {
+ return tools.filter((tool) => minimatch(tool.name, pattern, { nocase: true }));
+}
+
+/**
+ * Check if a pattern contains glob special characters.
+ */
+export function isGlobPattern(pattern: string): boolean {
+ return /[*?[\]{}!]/.test(pattern);
+}
diff --git a/src/cli/list-output.ts b/src/cli/list-output.ts
index 0153a3e..bd3bca9 100644
--- a/src/cli/list-output.ts
+++ b/src/cli/list-output.ts
@@ -5,6 +5,7 @@ import { formatErrorMessage, serializeConnectionIssue } from './json-output.js';
import { buildToolDoc } from './list-detail-helpers.js';
import type { ListSummaryResult, StatusCategory } from './list-format.js';
import { classifyListError } from './list-format.js';
+import { formatFunctionSignature } from './list-signature.js';
import { boldText, extraDimText } from './terminal.js';
import { formatTransportSummary } from './transport-utils.js';
@@ -119,6 +120,18 @@ function buildExampleOptions(
return undefined;
}
+/**
+ * Print tools as compact function signatures for --brief mode.
+ */
+export function printBriefToolList(metadataEntries: ToolMetadata[], options?: { colorize?: boolean }): void {
+ for (const entry of metadataEntries) {
+ const signature = formatFunctionSignature(entry.tool.name, entry.options, entry.tool.outputSchema, {
+ colorize: options?.colorize,
+ });
+ console.log(signature);
+ }
+}
+
export function createEmptyStatusCounts(): Record {
return {
ok: 0,
diff --git a/tests/cli-list-enhancement.test.ts b/tests/cli-list-enhancement.test.ts
new file mode 100644
index 0000000..a9f5d89
--- /dev/null
+++ b/tests/cli-list-enhancement.test.ts
@@ -0,0 +1,147 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { cliModulePromise } from './fixtures/cli-list-fixtures.js';
+
+describe('CLI list enhancement', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('extractListFlags', () => {
+ it('parses --brief flag', async () => {
+ const { extractListFlags } = await cliModulePromise;
+ const args = ['--brief', 'server'];
+ const flags = extractListFlags(args);
+ expect(flags.brief).toBe(true);
+ expect(args).toEqual(['server']);
+ });
+
+ it('throws error when --brief is used with --schema', async () => {
+ const { extractListFlags } = await cliModulePromise;
+ const args = ['--brief', '--schema', 'server'];
+ expect(() => extractListFlags(args)).toThrow(/--brief cannot be used with/);
+ });
+
+ it('throws error when --brief is used with --verbose', async () => {
+ const { extractListFlags } = await cliModulePromise;
+ const args = ['--brief', '--verbose', 'server'];
+ expect(() => extractListFlags(args)).toThrow(/--brief cannot be used with/);
+ });
+
+ it('throws error when --brief is used with --all-parameters (which disables requiredOnly)', async () => {
+ const { extractListFlags } = await cliModulePromise;
+ const args = ['--brief', '--all-parameters', 'server'];
+ expect(() => extractListFlags(args)).toThrow(/--brief cannot be used with/);
+ });
+ });
+
+ describe('handleList', () => {
+ const mockTools = [
+ {
+ name: 'add_user',
+ description: 'Add a user',
+ inputSchema: {
+ type: 'object',
+ properties: { name: { type: 'string' } },
+ },
+ },
+ {
+ name: 'list_users',
+ description: 'List users',
+ inputSchema: {
+ type: 'object',
+ properties: { limit: { type: 'number' } },
+ },
+ },
+ {
+ name: 'delete_user',
+ description: 'Delete a user',
+ inputSchema: {
+ type: 'object',
+ properties: { id: { type: 'string' } },
+ },
+ },
+ ];
+
+ const mockRuntime = {
+ getDefinitions: () => [],
+ getDefinition: (name: string) => ({
+ name,
+ command: { kind: 'stdio', command: 'noop' },
+ source: { kind: 'local', path: '/tmp/config.json' },
+ }),
+ listTools: () => Promise.resolve(mockTools),
+ };
+
+ it('filters tools by pattern', async () => {
+ const { handleList } = await cliModulePromise;
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ await handleList(mockRuntime as any, ['server', 'add_*']);
+
+ // Verify that output contains only matching tools
+ const calls = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
+ // This is checking text output logic for detailed view
+ // Since it's detailed view, it should print tool details
+ // Assuming printToolDetail behavior
+
+ // But verify we don't see delete_user
+ expect(calls).not.toContain('delete_user');
+ // We might not easily verify "add_user" presence without knowing exact output format detail line
+ // checking side effect: filterToolsByPattern works implies handleList integration works
+
+ consoleSpy.mockRestore();
+ });
+
+ it('prints brief signatures for --brief', async () => {
+ const { handleList } = await cliModulePromise;
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ await handleList(mockRuntime as any, ['server', '--brief']);
+
+ const calls = consoleSpy.mock.calls.map((c) => c[0]);
+ // Should match function signature format
+ expect(calls.some((line) => line.includes('function add_user(name?: string);'))).toBe(true);
+ expect(calls.some((line) => line.includes('function list_users(limit?: number);'))).toBe(true);
+ expect(calls.some((line) => line.includes('function delete_user(id?: string);'))).toBe(true);
+
+ consoleSpy.mockRestore();
+ });
+
+ it('filters and prints brief signatures combined', async () => {
+ const { handleList } = await cliModulePromise;
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ await handleList(mockRuntime as any, ['server', 'add_*', '--brief']);
+
+ const calls = consoleSpy.mock.calls.map((c) => c[0]);
+ expect(calls.some((line) => line.includes('function add_user'))).toBe(true);
+ expect(calls.some((line) => line.includes('function list_users'))).toBe(false);
+
+ consoleSpy.mockRestore();
+ });
+
+ it('handles no matches with pattern', async () => {
+ const { handleList } = await cliModulePromise;
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ await handleList(mockRuntime as any, ['server', 'nomatch_*']);
+
+ const calls = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
+ expect(calls).toContain("Tools: ");
+
+ consoleSpy.mockRestore();
+ });
+
+ it('handles no matches with pattern and brief', async () => {
+ const { handleList } = await cliModulePromise;
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ await handleList(mockRuntime as any, ['server', 'nomatch_*', '--brief']);
+
+ const calls = consoleSpy.mock.calls.map((c) => c[0]).join('\n');
+ expect(calls).toContain("No tools matching pattern 'nomatch_*'.");
+
+ consoleSpy.mockRestore();
+ });
+ });
+});
diff --git a/tests/cli-list-flags.test.ts b/tests/cli-list-flags.test.ts
index c755eb8..07a8f94 100644
--- a/tests/cli-list-flags.test.ts
+++ b/tests/cli-list-flags.test.ts
@@ -13,6 +13,7 @@ describe('CLI list flag parsing', () => {
requiredOnly: true,
includeSources: false,
verbose: false,
+ brief: false,
ephemeral: undefined,
format: 'text',
});
@@ -29,6 +30,7 @@ describe('CLI list flag parsing', () => {
requiredOnly: false,
includeSources: false,
verbose: false,
+ brief: false,
ephemeral: undefined,
format: 'text',
});