@crush-protocol/mcp-client 0.1.14 → 0.3.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.
@@ -0,0 +1,171 @@
1
+ import { type AuthResult } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import { type InteractiveOAuthProviderOptions } from "./oauthProvider.js";
3
+ import type { McpClientLike } from "./types.js";
4
+ export type OAuthRemoteMcpClientOptions = {
5
+ serverUrl: string;
6
+ clientName?: string;
7
+ clientVersion?: string;
8
+ scope?: string;
9
+ token?: string;
10
+ fetch?: typeof fetch;
11
+ oauth?: Omit<InteractiveOAuthProviderOptions, "serverUrl" | "clientName" | "clientVersion" | "scope">;
12
+ };
13
+ export declare class OAuthRemoteMcpClient implements McpClientLike {
14
+ private readonly options;
15
+ private readonly client;
16
+ private readonly transport;
17
+ private readonly authProvider;
18
+ constructor(options: OAuthRemoteMcpClientOptions);
19
+ connect(): Promise<void>;
20
+ ensureAuthorized(): Promise<void>;
21
+ runAuthFlow(): Promise<AuthResult>;
22
+ completeAuthorization(): Promise<void>;
23
+ close(): Promise<void>;
24
+ terminateSession(): Promise<void>;
25
+ ping(): Promise<{
26
+ _meta?: {
27
+ [x: string]: unknown;
28
+ progressToken?: string | number | undefined;
29
+ "io.modelcontextprotocol/related-task"?: {
30
+ taskId: string;
31
+ } | undefined;
32
+ } | undefined;
33
+ }>;
34
+ listTools(): Promise<{
35
+ [x: string]: unknown;
36
+ tools: {
37
+ inputSchema: {
38
+ [x: string]: unknown;
39
+ type: "object";
40
+ properties?: Record<string, object> | undefined;
41
+ required?: string[] | undefined;
42
+ };
43
+ name: string;
44
+ description?: string | undefined;
45
+ outputSchema?: {
46
+ [x: string]: unknown;
47
+ type: "object";
48
+ properties?: Record<string, object> | undefined;
49
+ required?: string[] | undefined;
50
+ } | undefined;
51
+ annotations?: {
52
+ title?: string | undefined;
53
+ readOnlyHint?: boolean | undefined;
54
+ destructiveHint?: boolean | undefined;
55
+ idempotentHint?: boolean | undefined;
56
+ openWorldHint?: boolean | undefined;
57
+ } | undefined;
58
+ execution?: {
59
+ taskSupport?: "optional" | "required" | "forbidden" | undefined;
60
+ } | undefined;
61
+ _meta?: Record<string, unknown> | undefined;
62
+ icons?: {
63
+ src: string;
64
+ mimeType?: string | undefined;
65
+ sizes?: string[] | undefined;
66
+ theme?: "light" | "dark" | undefined;
67
+ }[] | undefined;
68
+ title?: string | undefined;
69
+ }[];
70
+ _meta?: {
71
+ [x: string]: unknown;
72
+ progressToken?: string | number | undefined;
73
+ "io.modelcontextprotocol/related-task"?: {
74
+ taskId: string;
75
+ } | undefined;
76
+ } | undefined;
77
+ nextCursor?: string | undefined;
78
+ }>;
79
+ callTool(name: string, args?: Record<string, unknown>): Promise<{
80
+ [x: string]: unknown;
81
+ content: ({
82
+ type: "text";
83
+ text: string;
84
+ annotations?: {
85
+ audience?: ("user" | "assistant")[] | undefined;
86
+ priority?: number | undefined;
87
+ lastModified?: string | undefined;
88
+ } | undefined;
89
+ _meta?: Record<string, unknown> | undefined;
90
+ } | {
91
+ type: "image";
92
+ data: string;
93
+ mimeType: string;
94
+ annotations?: {
95
+ audience?: ("user" | "assistant")[] | undefined;
96
+ priority?: number | undefined;
97
+ lastModified?: string | undefined;
98
+ } | undefined;
99
+ _meta?: Record<string, unknown> | undefined;
100
+ } | {
101
+ type: "audio";
102
+ data: string;
103
+ mimeType: string;
104
+ annotations?: {
105
+ audience?: ("user" | "assistant")[] | undefined;
106
+ priority?: number | undefined;
107
+ lastModified?: string | undefined;
108
+ } | undefined;
109
+ _meta?: Record<string, unknown> | undefined;
110
+ } | {
111
+ type: "resource";
112
+ resource: {
113
+ uri: string;
114
+ text: string;
115
+ mimeType?: string | undefined;
116
+ _meta?: Record<string, unknown> | undefined;
117
+ } | {
118
+ uri: string;
119
+ blob: string;
120
+ mimeType?: string | undefined;
121
+ _meta?: Record<string, unknown> | undefined;
122
+ };
123
+ annotations?: {
124
+ audience?: ("user" | "assistant")[] | undefined;
125
+ priority?: number | undefined;
126
+ lastModified?: string | undefined;
127
+ } | undefined;
128
+ _meta?: Record<string, unknown> | undefined;
129
+ } | {
130
+ uri: string;
131
+ name: string;
132
+ type: "resource_link";
133
+ description?: string | undefined;
134
+ mimeType?: string | undefined;
135
+ annotations?: {
136
+ audience?: ("user" | "assistant")[] | undefined;
137
+ priority?: number | undefined;
138
+ lastModified?: string | undefined;
139
+ } | undefined;
140
+ _meta?: {
141
+ [x: string]: unknown;
142
+ } | undefined;
143
+ icons?: {
144
+ src: string;
145
+ mimeType?: string | undefined;
146
+ sizes?: string[] | undefined;
147
+ theme?: "light" | "dark" | undefined;
148
+ }[] | undefined;
149
+ title?: string | undefined;
150
+ })[];
151
+ _meta?: {
152
+ [x: string]: unknown;
153
+ progressToken?: string | number | undefined;
154
+ "io.modelcontextprotocol/related-task"?: {
155
+ taskId: string;
156
+ } | undefined;
157
+ } | undefined;
158
+ structuredContent?: Record<string, unknown> | undefined;
159
+ isError?: boolean | undefined;
160
+ } | {
161
+ [x: string]: unknown;
162
+ toolResult: unknown;
163
+ _meta?: {
164
+ [x: string]: unknown;
165
+ progressToken?: string | number | undefined;
166
+ "io.modelcontextprotocol/related-task"?: {
167
+ taskId: string;
168
+ } | undefined;
169
+ } | undefined;
170
+ }>;
171
+ }
@@ -0,0 +1,95 @@
1
+ import { auth, UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { InteractiveOAuthProvider } from "./oauthProvider.js";
5
+ export class OAuthRemoteMcpClient {
6
+ options;
7
+ client;
8
+ transport;
9
+ authProvider;
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.client = new Client({
13
+ name: options.clientName ?? "crush-mcp-client",
14
+ version: options.clientVersion ?? "0.2.0",
15
+ }, {
16
+ capabilities: {},
17
+ });
18
+ this.authProvider = new InteractiveOAuthProvider({
19
+ serverUrl: options.serverUrl,
20
+ clientName: options.clientName,
21
+ clientVersion: options.clientVersion,
22
+ scope: options.scope,
23
+ ...options.oauth,
24
+ });
25
+ this.transport = new StreamableHTTPClientTransport(new URL(options.serverUrl), {
26
+ authProvider: this.authProvider,
27
+ requestInit: options.token
28
+ ? {
29
+ headers: {
30
+ Authorization: `Bearer ${options.token}`,
31
+ },
32
+ }
33
+ : undefined,
34
+ fetch: options.fetch,
35
+ });
36
+ }
37
+ async connect() {
38
+ try {
39
+ await this.client.connect(this.transport);
40
+ }
41
+ catch (error) {
42
+ if (!(error instanceof UnauthorizedError)) {
43
+ throw error;
44
+ }
45
+ const authResult = await auth(this.authProvider, {
46
+ serverUrl: this.options.serverUrl,
47
+ scope: this.options.scope,
48
+ fetchFn: this.options.fetch,
49
+ });
50
+ if (authResult !== "REDIRECT") {
51
+ await this.client.connect(this.transport);
52
+ return;
53
+ }
54
+ const authorizationCode = await this.authProvider.waitForAuthorizationCode();
55
+ await this.transport.finishAuth(authorizationCode);
56
+ await this.client.connect(this.transport);
57
+ }
58
+ }
59
+ async ensureAuthorized() {
60
+ const authResult = await this.runAuthFlow();
61
+ if (authResult === "REDIRECT") {
62
+ await this.completeAuthorization();
63
+ }
64
+ }
65
+ async runAuthFlow() {
66
+ return auth(this.authProvider, {
67
+ serverUrl: this.options.serverUrl,
68
+ scope: this.options.scope,
69
+ fetchFn: this.options.fetch,
70
+ });
71
+ }
72
+ async completeAuthorization() {
73
+ const authorizationCode = await this.authProvider.waitForAuthorizationCode();
74
+ await this.transport.finishAuth(authorizationCode);
75
+ }
76
+ async close() {
77
+ await this.authProvider.close();
78
+ await this.transport.close();
79
+ }
80
+ async terminateSession() {
81
+ await this.transport.terminateSession();
82
+ }
83
+ async ping() {
84
+ return this.client.ping();
85
+ }
86
+ async listTools() {
87
+ return this.client.listTools();
88
+ }
89
+ async callTool(name, args = {}) {
90
+ return this.client.callTool({
91
+ name,
92
+ arguments: args,
93
+ });
94
+ }
95
+ }
@@ -1,10 +1,11 @@
1
+ import type { McpClientLike } from "./types.js";
1
2
  export type RemoteMcpClientOptions = {
2
3
  serverUrl: string;
3
4
  token: string;
4
5
  clientName?: string;
5
6
  clientVersion?: string;
6
7
  };
7
- export declare class RemoteMcpClient {
8
+ export declare class RemoteMcpClient implements McpClientLike {
8
9
  private readonly client;
9
10
  private readonly transport;
10
11
  constructor(options: RemoteMcpClientOptions);
@@ -4,8 +4,8 @@ export class RemoteMcpClient {
4
4
  client;
5
5
  transport;
6
6
  constructor(options) {
7
- if (!options.token.startsWith("mcp_")) {
8
- throw new Error("Invalid token format. Expected mcp_xxx");
7
+ if (typeof options.token !== "string" || options.token.trim().length === 0) {
8
+ throw new Error("Invalid token. Expected a non-empty OAuth access token");
9
9
  }
10
10
  this.client = new Client({
11
11
  name: options.clientName || "crush-mcp-client",
@@ -0,0 +1,8 @@
1
+ export interface McpClientLike {
2
+ connect(): Promise<void>;
3
+ close(): Promise<void>;
4
+ terminateSession(): Promise<void>;
5
+ ping(): Promise<unknown>;
6
+ listTools(): Promise<unknown>;
7
+ callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export type SetupTarget = "cursor" | "claude" | "codex" | "gemini" | "opencode";
2
+ export type SetupScope = "user" | "project";
3
+ export declare const installClientConfig: (target: SetupTarget, serverUrl: string, scope: SetupScope) => string;
@@ -0,0 +1,119 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const SERVER_NAME = "crush-protocol";
6
+ const PACKAGE_NAME = "@crush-protocol/mcp-client";
7
+ const ensureDir = (filePath) => {
8
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
9
+ };
10
+ const readJson = (filePath) => {
11
+ if (!fs.existsSync(filePath))
12
+ return {};
13
+ try {
14
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
15
+ }
16
+ catch (error) {
17
+ throw new Error(`Failed to parse JSON config at ${filePath}: ${error.message}`);
18
+ }
19
+ };
20
+ const writeJson = (filePath, value) => {
21
+ ensureDir(filePath);
22
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
23
+ };
24
+ const createNpxConfig = (serverUrl) => ({
25
+ command: "npx",
26
+ args: ["-y", PACKAGE_NAME, "--url", serverUrl],
27
+ });
28
+ const getCursorConfigPath = (scope) => scope === "project"
29
+ ? path.join(process.cwd(), ".cursor", "mcp.json")
30
+ : path.join(os.homedir(), ".cursor", "mcp.json");
31
+ const getGeminiConfigPath = () => path.join(os.homedir(), ".gemini", "settings.json");
32
+ const getOpenCodeConfigPath = (scope) => scope === "project"
33
+ ? path.join(process.cwd(), "opencode.json")
34
+ : path.join(os.homedir(), ".config", "opencode", "opencode.json");
35
+ const getCodexConfigPath = () => path.join(os.homedir(), ".codex", "config.toml");
36
+ const installCursor = (serverUrl, scope) => {
37
+ const filePath = getCursorConfigPath(scope);
38
+ const config = readJson(filePath);
39
+ const mcpServers = (config.mcpServers ?? {});
40
+ mcpServers[SERVER_NAME] = createNpxConfig(serverUrl);
41
+ config.mcpServers = mcpServers;
42
+ writeJson(filePath, config);
43
+ return filePath;
44
+ };
45
+ const installGemini = (serverUrl) => {
46
+ const filePath = getGeminiConfigPath();
47
+ const config = readJson(filePath);
48
+ const mcpServers = (config.mcpServers ?? {});
49
+ mcpServers[SERVER_NAME] = createNpxConfig(serverUrl);
50
+ config.mcpServers = mcpServers;
51
+ writeJson(filePath, config);
52
+ return filePath;
53
+ };
54
+ const installOpenCode = (serverUrl, scope) => {
55
+ const filePath = getOpenCodeConfigPath(scope);
56
+ const config = readJson(filePath);
57
+ const mcp = (config.mcp ?? {});
58
+ if (typeof config.$schema !== "string") {
59
+ config.$schema = "https://opencode.ai/config.json";
60
+ }
61
+ mcp[SERVER_NAME] = {
62
+ type: "local",
63
+ command: ["npx", "-y", PACKAGE_NAME, "--url", serverUrl],
64
+ enabled: true,
65
+ };
66
+ config.mcp = mcp;
67
+ writeJson(filePath, config);
68
+ return filePath;
69
+ };
70
+ const installCodex = (serverUrl) => {
71
+ const filePath = getCodexConfigPath();
72
+ const section = `[mcp_servers.${SERVER_NAME}]
73
+ command = "npx"
74
+ args = ["-y", "${PACKAGE_NAME}", "--url", "${serverUrl}"]
75
+ startup_timeout_ms = 20000
76
+ `;
77
+ ensureDir(filePath);
78
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
79
+ if (!existing.includes(`[mcp_servers.${SERVER_NAME}]`)) {
80
+ const next = existing.trim().length > 0 ? `${existing.trimEnd()}\n\n${section}` : section;
81
+ fs.writeFileSync(filePath, next, "utf8");
82
+ }
83
+ return filePath;
84
+ };
85
+ const installClaude = (serverUrl, scope) => {
86
+ const configJson = JSON.stringify({
87
+ type: "stdio",
88
+ command: "npx",
89
+ args: ["-y", PACKAGE_NAME, "--url", serverUrl],
90
+ });
91
+ const result = spawnSync("claude", ["mcp", "add-json", "--scope", scope, SERVER_NAME, configJson], {
92
+ stdio: "pipe",
93
+ encoding: "utf8",
94
+ });
95
+ if (result.error) {
96
+ throw new Error(`Failed to run Claude CLI: ${result.error.message}`);
97
+ }
98
+ if (result.status !== 0) {
99
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
100
+ throw new Error(output || "Claude CLI returned a non-zero exit code.");
101
+ }
102
+ return "claude-managed-config";
103
+ };
104
+ export const installClientConfig = (target, serverUrl, scope) => {
105
+ switch (target) {
106
+ case "cursor":
107
+ return installCursor(serverUrl, scope);
108
+ case "claude":
109
+ return installClaude(serverUrl, scope);
110
+ case "codex":
111
+ return installCodex(serverUrl);
112
+ case "gemini":
113
+ return installGemini(serverUrl);
114
+ case "opencode":
115
+ return installOpenCode(serverUrl, scope);
116
+ default:
117
+ throw new Error(`Unsupported setup target: ${target}`);
118
+ }
119
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crush-protocol/mcp-client",
3
- "version": "0.1.14",
3
+ "version": "0.3.0",
4
4
  "description": "Crush MCP npm client package (remote Streamable HTTP + optional ClickHouse direct)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,6 +13,9 @@
13
13
  "dist",
14
14
  "INSTRUCTIONS.md"
15
15
  ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
16
19
  "exports": {
17
20
  ".": {
18
21
  "types": "./dist/index.d.ts",
@@ -23,7 +26,7 @@
23
26
  "@modelcontextprotocol/sdk": "^1.26.0",
24
27
  "dotenv": "^17.2.1",
25
28
  "zod": "^3.25.76",
26
- "@crush-protocol/mcp-contracts": "0.1.0"
29
+ "@crush-protocol/mcp-contracts": "0.1.2"
27
30
  },
28
31
  "devDependencies": {
29
32
  "@types/node": "^24.3.0",
@@ -38,6 +41,7 @@
38
41
  "scripts": {
39
42
  "build": "tsc -p tsconfig.json",
40
43
  "dev": "tsx src/cli.ts",
44
+ "test": "vitest run",
41
45
  "test:e2e": "dotenv -e .env.e2e vitest run src/__tests__/e2e.test.ts"
42
46
  }
43
47
  }