@evantahler/mcpx 0.17.1 → 0.18.0
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.
- package/.claude/skills/mcpx.md +16 -0
- package/.cursor/rules/mcpx.mdc +16 -0
- package/README.md +49 -3
- package/package.json +7 -1
- package/src/sdk.ts +310 -0
package/.claude/skills/mcpx.md
CHANGED
|
@@ -231,6 +231,22 @@ mcpx deauth <server> # remove stored auth
|
|
|
231
231
|
| `--project` | Install to project location (default) |
|
|
232
232
|
| `-f, --force` | Overwrite if file already exists |
|
|
233
233
|
|
|
234
|
+
## Programmatic Usage (TypeScript SDK)
|
|
235
|
+
|
|
236
|
+
For agents that don't have shell access (remote, persistent, or isolated agents), mcpx can be used as a TypeScript library:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { McpxClient } from "@evantahler/mcpx";
|
|
240
|
+
|
|
241
|
+
const client = new McpxClient();
|
|
242
|
+
const results = await client.search("send a message");
|
|
243
|
+
const tool = await client.info("arcade", "Slack_SendMessage");
|
|
244
|
+
const result = await client.exec("arcade", "Slack_SendMessage", { channel: "#general", message: "hello" });
|
|
245
|
+
await client.close();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The SDK follows the same search → inspect → exec workflow. Server management (add, remove, auth) is still done via the CLI.
|
|
249
|
+
|
|
234
250
|
## Environment variables
|
|
235
251
|
|
|
236
252
|
| Variable | Purpose | Default |
|
package/.cursor/rules/mcpx.mdc
CHANGED
|
@@ -225,6 +225,22 @@ mcpx deauth <server> # remove stored auth
|
|
|
225
225
|
| `--project` | Install to project location (default) |
|
|
226
226
|
| `-f, --force` | Overwrite if file already exists |
|
|
227
227
|
|
|
228
|
+
## Programmatic Usage (TypeScript SDK)
|
|
229
|
+
|
|
230
|
+
For agents that don't have shell access (remote, persistent, or isolated agents), mcpx can be used as a TypeScript library:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { McpxClient } from "@evantahler/mcpx";
|
|
234
|
+
|
|
235
|
+
const client = new McpxClient();
|
|
236
|
+
const results = await client.search("send a message");
|
|
237
|
+
const tool = await client.info("arcade", "Slack_SendMessage");
|
|
238
|
+
const result = await client.exec("arcade", "Slack_SendMessage", { channel: "#general", message: "hello" });
|
|
239
|
+
await client.close();
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The SDK follows the same search → inspect → exec workflow. Server management (add, remove, auth) is still done via the CLI.
|
|
243
|
+
|
|
228
244
|
## Environment variables
|
|
229
245
|
|
|
230
246
|
| Variable | Purpose | Default |
|
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@ A command-line interface for MCP servers. **curl for MCP.**
|
|
|
4
4
|
|
|
5
5
|
The internet is debating CLI vs MCP like they're competitors. [They're not.](https://arcade.dev/blog/curl-for-mcp)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Three audiences:
|
|
8
8
|
|
|
9
|
-
1. **
|
|
10
|
-
2. **
|
|
9
|
+
1. **Coding agents** (Claude Code, Cursor) that prefer shelling out over maintaining persistent MCP connections — better for token management, progressive tool discovery, and sharing a single pool of MCP servers across multiple agents on one machine
|
|
10
|
+
2. **Non-coding agents** that need programmatic access to MCP tools from TypeScript — remote, persistent, or isolated agents that don't have a shell
|
|
11
|
+
3. **MCP developers** who need a fast way to discover, debug, and test their servers from the terminal
|
|
11
12
|
|
|
12
13
|
## Install
|
|
13
14
|
|
|
@@ -635,6 +636,51 @@ To execute tools:
|
|
|
635
636
|
Always search before executing — don't assume tool names.
|
|
636
637
|
```
|
|
637
638
|
|
|
639
|
+
### Programmatic Usage (TypeScript SDK)
|
|
640
|
+
|
|
641
|
+
For agents that don't have shell access — remote, persistent, or isolated agents running in TypeScript:
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
import { McpxClient } from "@evantahler/mcpx";
|
|
645
|
+
|
|
646
|
+
const client = new McpxClient();
|
|
647
|
+
// or: new McpxClient({ configDir: "/path/to/.mcpx" })
|
|
648
|
+
// or: new McpxClient({ servers: { mcpServers: { ... } } })
|
|
649
|
+
|
|
650
|
+
// 1. Search for tools
|
|
651
|
+
const results = await client.search("send a message");
|
|
652
|
+
|
|
653
|
+
// 2. Inspect the tool schema
|
|
654
|
+
const tool = await client.info("arcade", "Slack_SendMessage");
|
|
655
|
+
|
|
656
|
+
// 3. Execute the tool
|
|
657
|
+
const result = await client.exec("arcade", "Slack_SendMessage", {
|
|
658
|
+
channel: "#general",
|
|
659
|
+
message: "hello",
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// Also available: listTools, listResources, readResource,
|
|
663
|
+
// listPrompts, getPrompt, listTasks, getTask, cancelTask,
|
|
664
|
+
// getServerInfo, getServerNames, validateToolInput
|
|
665
|
+
|
|
666
|
+
await client.close();
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
The SDK uses the same config files as the CLI (`~/.mcpx/servers.json`, `auth.json`, `search.json`). Server management (add, remove, auth) is done via the CLI — the SDK is read-only.
|
|
670
|
+
|
|
671
|
+
You can also pass server config directly, bypassing file loading entirely:
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
const client = new McpxClient({
|
|
675
|
+
servers: {
|
|
676
|
+
mcpServers: {
|
|
677
|
+
local: { command: "node", args: ["server.js"] },
|
|
678
|
+
remote: { url: "https://mcp.example.com" },
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
```
|
|
683
|
+
|
|
638
684
|
## Permissions (Claude Code & Cursor)
|
|
639
685
|
|
|
640
686
|
AI agents like Claude Code and Cursor prompt users to approve each `mcpx exec` call. `mcpx allow` and `mcpx deny` manage fine-grained permission rules so agents can self-authorize specific tools without broad access.
|
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evantahler/mcpx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "A command-line interface for MCP servers. curl for MCP.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/sdk.ts",
|
|
8
|
+
"./cli": "./src/cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/sdk.ts",
|
|
11
|
+
"types": "./src/sdk.ts",
|
|
6
12
|
"bin": {
|
|
7
13
|
"mcpx": "./src/cli.ts"
|
|
8
14
|
},
|
package/src/sdk.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallToolResult,
|
|
3
|
+
GetTaskResult,
|
|
4
|
+
ListTasksResult,
|
|
5
|
+
CancelTaskResult,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { ServerManager } from "./client/manager.ts";
|
|
8
|
+
import type {
|
|
9
|
+
ServerManagerOptions,
|
|
10
|
+
ToolWithServer,
|
|
11
|
+
ResourceWithServer,
|
|
12
|
+
PromptWithServer,
|
|
13
|
+
ServerInfo,
|
|
14
|
+
ServerError,
|
|
15
|
+
} from "./client/manager.ts";
|
|
16
|
+
import { loadConfig } from "./config/loader.ts";
|
|
17
|
+
import type {
|
|
18
|
+
Tool,
|
|
19
|
+
Resource,
|
|
20
|
+
Prompt,
|
|
21
|
+
ServerConfig,
|
|
22
|
+
StdioServerConfig,
|
|
23
|
+
HttpServerConfig,
|
|
24
|
+
ServersFile,
|
|
25
|
+
AuthFile,
|
|
26
|
+
AuthEntry,
|
|
27
|
+
SearchIndex,
|
|
28
|
+
Config,
|
|
29
|
+
} from "./config/schemas.ts";
|
|
30
|
+
import { search } from "./search/index.ts";
|
|
31
|
+
import type { SearchResult, SearchOptions } from "./search/index.ts";
|
|
32
|
+
import { validateToolInput } from "./validation/schema.ts";
|
|
33
|
+
import type { ValidationResult, ValidationError } from "./validation/schema.ts";
|
|
34
|
+
|
|
35
|
+
// Re-export types for SDK consumers
|
|
36
|
+
export type {
|
|
37
|
+
// MCP SDK types
|
|
38
|
+
CallToolResult,
|
|
39
|
+
GetTaskResult,
|
|
40
|
+
ListTasksResult,
|
|
41
|
+
CancelTaskResult,
|
|
42
|
+
// Config types
|
|
43
|
+
Tool,
|
|
44
|
+
Resource,
|
|
45
|
+
Prompt,
|
|
46
|
+
ServerConfig,
|
|
47
|
+
StdioServerConfig,
|
|
48
|
+
HttpServerConfig,
|
|
49
|
+
ServersFile,
|
|
50
|
+
AuthFile,
|
|
51
|
+
AuthEntry,
|
|
52
|
+
SearchIndex,
|
|
53
|
+
Config,
|
|
54
|
+
// Manager types
|
|
55
|
+
ToolWithServer,
|
|
56
|
+
ResourceWithServer,
|
|
57
|
+
PromptWithServer,
|
|
58
|
+
ServerInfo,
|
|
59
|
+
ServerError,
|
|
60
|
+
// Search types
|
|
61
|
+
SearchResult,
|
|
62
|
+
SearchOptions,
|
|
63
|
+
// Validation types
|
|
64
|
+
ValidationResult,
|
|
65
|
+
ValidationError,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface McpxClientOptions {
|
|
69
|
+
/** Path to config directory. Defaults to ~/.mcpx or MCP_CONFIG_PATH env var. */
|
|
70
|
+
configDir?: string;
|
|
71
|
+
/** Inline server config — bypasses file loading when provided. */
|
|
72
|
+
servers?: ServersFile;
|
|
73
|
+
/** Inline auth config — bypasses file loading when provided. */
|
|
74
|
+
auth?: AuthFile;
|
|
75
|
+
/** Inline search index — bypasses file loading when provided. */
|
|
76
|
+
searchIndex?: SearchIndex;
|
|
77
|
+
/** Max concurrent server connections. Default: 5 */
|
|
78
|
+
concurrency?: number;
|
|
79
|
+
/** Request timeout in ms. Default: 1_800_000 (30 min) */
|
|
80
|
+
timeout?: number;
|
|
81
|
+
/** Max retries per operation. Default: 3 */
|
|
82
|
+
maxRetries?: number;
|
|
83
|
+
/** Enable verbose/trace logging. Default: false */
|
|
84
|
+
verbose?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class McpxClient {
|
|
88
|
+
private options: McpxClientOptions;
|
|
89
|
+
private manager: ServerManager | undefined;
|
|
90
|
+
private searchIndex: SearchIndex | undefined;
|
|
91
|
+
private connectPromise: Promise<void> | undefined;
|
|
92
|
+
|
|
93
|
+
constructor(options: McpxClientOptions = {}) {
|
|
94
|
+
this.options = options;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Ensure config is loaded and ServerManager is ready. Idempotent. */
|
|
98
|
+
private async ensureConnected(): Promise<ServerManager> {
|
|
99
|
+
if (this.manager) return this.manager;
|
|
100
|
+
|
|
101
|
+
if (!this.connectPromise) {
|
|
102
|
+
this.connectPromise = this.init();
|
|
103
|
+
}
|
|
104
|
+
await this.connectPromise;
|
|
105
|
+
return this.manager!;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async init(): Promise<void> {
|
|
109
|
+
let servers: ServersFile;
|
|
110
|
+
let auth: AuthFile;
|
|
111
|
+
let configDir: string;
|
|
112
|
+
let searchIndex: SearchIndex;
|
|
113
|
+
|
|
114
|
+
if (this.options.servers) {
|
|
115
|
+
// Inline config — no file loading
|
|
116
|
+
servers = this.options.servers;
|
|
117
|
+
auth = this.options.auth ?? {};
|
|
118
|
+
configDir = this.options.configDir ?? "/tmp";
|
|
119
|
+
searchIndex = this.options.searchIndex ?? {
|
|
120
|
+
version: 1,
|
|
121
|
+
indexed_at: "",
|
|
122
|
+
embedding_model: "",
|
|
123
|
+
tools: [],
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
// Load from disk
|
|
127
|
+
const config = await loadConfig({ configFlag: this.options.configDir });
|
|
128
|
+
servers = config.servers;
|
|
129
|
+
auth = config.auth;
|
|
130
|
+
configDir = config.configDir;
|
|
131
|
+
searchIndex = config.searchIndex;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.searchIndex = searchIndex;
|
|
135
|
+
|
|
136
|
+
const managerOpts: ServerManagerOptions = {
|
|
137
|
+
servers,
|
|
138
|
+
configDir,
|
|
139
|
+
auth,
|
|
140
|
+
concurrency: this.options.concurrency,
|
|
141
|
+
verbose: this.options.verbose,
|
|
142
|
+
timeout: this.options.timeout,
|
|
143
|
+
maxRetries: this.options.maxRetries,
|
|
144
|
+
logLevel: "emergency", // suppress server log messages from writing to stderr
|
|
145
|
+
noInteractive: true, // agents can't fill elicitation forms
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.manager = new ServerManager(managerOpts);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Core workflow: search → info → exec
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/** Search for tools by keyword and/or semantic similarity. Requires a pre-built index (run `mcpx index` via CLI). */
|
|
156
|
+
async search(query: string, options?: SearchOptions): Promise<SearchResult[]> {
|
|
157
|
+
await this.ensureConnected();
|
|
158
|
+
if (!this.searchIndex || this.searchIndex.tools.length === 0) {
|
|
159
|
+
throw new Error("No search index found. Build one with: mcpx index");
|
|
160
|
+
}
|
|
161
|
+
return search(query, this.searchIndex, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Get a tool's schema (name, description, inputSchema). */
|
|
165
|
+
async info(server: string, tool: string): Promise<Tool | undefined> {
|
|
166
|
+
const manager = await this.ensureConnected();
|
|
167
|
+
return manager.getToolSchema(server, tool);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Execute a tool and return the result. */
|
|
171
|
+
async exec(
|
|
172
|
+
server: string,
|
|
173
|
+
tool: string,
|
|
174
|
+
args?: Record<string, unknown>,
|
|
175
|
+
): Promise<CallToolResult> {
|
|
176
|
+
const manager = await this.ensureConnected();
|
|
177
|
+
return manager.callTool(server, tool, args ?? {}) as Promise<CallToolResult>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Tools
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/** List tools, optionally filtered to a single server. */
|
|
185
|
+
async listTools(server?: string): Promise<ToolWithServer[]> {
|
|
186
|
+
const manager = await this.ensureConnected();
|
|
187
|
+
if (server) {
|
|
188
|
+
const tools = await manager.listTools(server);
|
|
189
|
+
return tools.map((tool) => ({ server, tool }));
|
|
190
|
+
}
|
|
191
|
+
const { tools } = await manager.getAllTools();
|
|
192
|
+
return tools;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Validation
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/** Validate arguments against a tool's inputSchema. */
|
|
200
|
+
async validateToolInput(
|
|
201
|
+
server: string,
|
|
202
|
+
toolName: string,
|
|
203
|
+
args: Record<string, unknown>,
|
|
204
|
+
): Promise<ValidationResult> {
|
|
205
|
+
const tool = await this.info(server, toolName);
|
|
206
|
+
if (!tool) {
|
|
207
|
+
return { valid: false, errors: [{ path: "(root)", message: `Tool not found: ${toolName}` }] };
|
|
208
|
+
}
|
|
209
|
+
return validateToolInput(server, tool, args);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Resources
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/** List resources, optionally filtered to a single server. */
|
|
217
|
+
async listResources(server?: string): Promise<ResourceWithServer[]> {
|
|
218
|
+
const manager = await this.ensureConnected();
|
|
219
|
+
if (server) {
|
|
220
|
+
const resources = await manager.listResources(server);
|
|
221
|
+
return resources.map((resource) => ({ server, resource }));
|
|
222
|
+
}
|
|
223
|
+
const { resources } = await manager.getAllResources();
|
|
224
|
+
return resources;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Read a specific resource by URI. */
|
|
228
|
+
async readResource(server: string, uri: string): Promise<unknown> {
|
|
229
|
+
const manager = await this.ensureConnected();
|
|
230
|
+
return manager.readResource(server, uri);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Prompts
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** List prompts, optionally filtered to a single server. */
|
|
238
|
+
async listPrompts(server?: string): Promise<PromptWithServer[]> {
|
|
239
|
+
const manager = await this.ensureConnected();
|
|
240
|
+
if (server) {
|
|
241
|
+
const prompts = await manager.listPrompts(server);
|
|
242
|
+
return prompts.map((prompt) => ({ server, prompt }));
|
|
243
|
+
}
|
|
244
|
+
const { prompts } = await manager.getAllPrompts();
|
|
245
|
+
return prompts;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Get a specific prompt by name, optionally with arguments. */
|
|
249
|
+
async getPrompt(server: string, name: string, args?: Record<string, string>): Promise<unknown> {
|
|
250
|
+
const manager = await this.ensureConnected();
|
|
251
|
+
return manager.getPrompt(server, name, args);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Tasks
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/** List tasks on a server. */
|
|
259
|
+
async listTasks(server: string, cursor?: string): Promise<ListTasksResult> {
|
|
260
|
+
const manager = await this.ensureConnected();
|
|
261
|
+
return manager.listTasks(server, cursor);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Get the status of a task. */
|
|
265
|
+
async getTask(server: string, taskId: string): Promise<GetTaskResult> {
|
|
266
|
+
const manager = await this.ensureConnected();
|
|
267
|
+
return manager.getTask(server, taskId);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Retrieve the result of a completed task. */
|
|
271
|
+
async getTaskResult(server: string, taskId: string): Promise<CallToolResult> {
|
|
272
|
+
const manager = await this.ensureConnected();
|
|
273
|
+
return manager.getTaskResult(server, taskId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Cancel a running task. */
|
|
277
|
+
async cancelTask(server: string, taskId: string): Promise<CancelTaskResult> {
|
|
278
|
+
const manager = await this.ensureConnected();
|
|
279
|
+
return manager.cancelTask(server, taskId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Server info
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
/** Get server info (version, capabilities, instructions). */
|
|
287
|
+
async getServerInfo(server: string): Promise<ServerInfo> {
|
|
288
|
+
const manager = await this.ensureConnected();
|
|
289
|
+
return manager.getServerInfo(server);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Get all configured server names. */
|
|
293
|
+
async getServerNames(): Promise<string[]> {
|
|
294
|
+
const manager = await this.ensureConnected();
|
|
295
|
+
return manager.getServerNames();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Lifecycle
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/** Disconnect all servers and clean up. */
|
|
303
|
+
async close(): Promise<void> {
|
|
304
|
+
if (this.manager) {
|
|
305
|
+
await this.manager.close();
|
|
306
|
+
this.manager = undefined;
|
|
307
|
+
this.connectPromise = undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|