@evantahler/mcpx 0.17.1 → 0.18.1

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.
@@ -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 |
@@ -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
- Two audiences:
7
+ Three audiences:
8
8
 
9
- 1. **AI/LLM agents** 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. **MCP developers** who need a fast way to discover, debug, and test their servers from the terminal
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.17.1",
3
+ "version": "0.18.1",
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
+ }