@hasna/switcher 0.1.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,24 @@
1
+ import type { Model, Provider, Profile } from "./domain";
2
+ export type HarnessId = Profile["harness"];
3
+ export type HarnessLaunchInput = {
4
+ harness: HarnessId;
5
+ baseUrl: string;
6
+ protocol: Provider["protocol"];
7
+ authStyle?: Provider["authStyle"];
8
+ model: string;
9
+ models: Model[];
10
+ credential?: string;
11
+ executable?: string;
12
+ args?: string[];
13
+ stateDir: string;
14
+ cwd: string;
15
+ version?: string;
16
+ };
17
+ export type PreparedLaunch = {
18
+ executable: string;
19
+ args: string[];
20
+ env: Record<string, string>;
21
+ configPaths: string[];
22
+ warnings: string[];
23
+ cleanup?: () => Promise<void>;
24
+ };
@@ -0,0 +1,13 @@
1
+ import type { HarnessId, HarnessLaunchInput, PreparedLaunch } from "./harness-types";
2
+ export declare function detectHarness(harness: HarnessId, override?: string): Promise<{
3
+ harness: "claude" | "codex" | "grok" | "opencode2";
4
+ executable: string;
5
+ available: boolean;
6
+ version: string;
7
+ } | {
8
+ harness: "claude" | "codex" | "grok" | "opencode2";
9
+ executable: string;
10
+ available: boolean;
11
+ version: undefined;
12
+ }>;
13
+ export declare function prepareHarnessLaunch(input: HarnessLaunchInput): Promise<PreparedLaunch>;
package/dist/http.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function boundedJson(response: Pick<Response, "body">, maxBytes?: number): Promise<any>;
@@ -0,0 +1 @@
1
+ export * from "./sdk";
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ // src/domain.ts
2
+ import { z } from "zod";
3
+ var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
4
+ var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
5
+ var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
6
+ var label = z.string().min(1).max(200);
7
+ var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
8
+ function endpoint(value) {
9
+ let url;
10
+ try {
11
+ url = new URL(value);
12
+ } catch {
13
+ throw new Fault(400, "invalid_url", "Use an absolute HTTPS URL (HTTP is allowed on loopback).");
14
+ }
15
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
16
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local) || url.username || url.password || url.search || url.hash)
17
+ throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
18
+ return url.href.replace(/\/+$/, "");
19
+ }
20
+ var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
21
+ try {
22
+ endpoint(v);
23
+ } catch {
24
+ ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
25
+ }
26
+ }).transform(endpoint);
27
+ var modelSchema = z.object({
28
+ id: z.string().min(1).max(300),
29
+ name: label,
30
+ description: z.string().max(8000).optional(),
31
+ contextWindow: z.number().int().positive().optional(),
32
+ maxOutputTokens: z.number().int().positive().optional(),
33
+ inputModalities: z.array(z.string().max(50)).max(20).optional(),
34
+ outputModalities: z.array(z.string().max(50)).max(20).optional(),
35
+ supportedParameters: z.array(z.string().max(100)).max(100).optional()
36
+ }).strict();
37
+ var providerInputSchema = z.object({
38
+ id: idSchema,
39
+ name: label,
40
+ baseUrl: urlSchema,
41
+ protocol: protocolSchema,
42
+ credentialEnv: envRef.optional(),
43
+ authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
44
+ modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
45
+ manualModels: z.array(modelSchema).max(1e4).default([])
46
+ }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
47
+ var profileInputSchema = z.object({
48
+ id: idSchema,
49
+ name: label,
50
+ providerId: idSchema,
51
+ harness: harnessSchema,
52
+ model: z.string().min(1).max(300)
53
+ }).strict();
54
+ var runInputSchema = z.object({
55
+ profileId: idSchema,
56
+ harness: harnessSchema,
57
+ model: z.string().min(1).max(300),
58
+ planToken: z.string().regex(/^[a-f0-9]{64}$/)
59
+ }).strict();
60
+ var runUpdateSchema = z.object({
61
+ status: z.enum(["exited", "failed", "interrupted"]),
62
+ exitCode: z.number().int().min(0).max(255)
63
+ }).strict();
64
+
65
+ class Fault extends Error {
66
+ status;
67
+ code;
68
+ constructor(status, code, message) {
69
+ super(message);
70
+ this.status = status;
71
+ this.code = code;
72
+ }
73
+ }
74
+
75
+ // src/http.ts
76
+ var MAX_BYTES = 16 * 1024 * 1024;
77
+ async function boundedJson(response, maxBytes = MAX_BYTES) {
78
+ if (!response.body)
79
+ throw new Fault(502, "invalid_upstream", "Upstream returned no body.");
80
+ const reader = response.body.getReader();
81
+ const chunks = [];
82
+ let size = 0;
83
+ try {
84
+ while (true) {
85
+ const item = await reader.read();
86
+ if (item.done)
87
+ break;
88
+ size += item.value.byteLength;
89
+ if (size > maxBytes)
90
+ throw new Fault(502, "response_too_large", "Response exceeds the size limit.");
91
+ chunks.push(item.value);
92
+ }
93
+ const bytes = new Uint8Array(size);
94
+ let offset = 0;
95
+ for (const chunk of chunks) {
96
+ bytes.set(chunk, offset);
97
+ offset += chunk.length;
98
+ }
99
+ try {
100
+ return JSON.parse(new TextDecoder().decode(bytes));
101
+ } catch {
102
+ throw new Fault(502, "invalid_upstream", "Upstream returned invalid JSON.");
103
+ }
104
+ } finally {
105
+ await reader.cancel().catch(() => {});
106
+ }
107
+ }
108
+
109
+ // src/sdk.ts
110
+ import { resolveCredential } from "@hasna/contracts/client";
111
+
112
+ class SwitcherError extends Error {
113
+ status;
114
+ code;
115
+ requestId;
116
+ constructor(status, code, message, requestId) {
117
+ super(message);
118
+ this.status = status;
119
+ this.code = code;
120
+ this.requestId = requestId;
121
+ }
122
+ }
123
+
124
+ class SwitcherClient {
125
+ options;
126
+ baseUrl;
127
+ constructor(options) {
128
+ this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
129
+ if (typeof options.apiKey === "string" && (!options.apiKey || /[\r\n]/.test(options.apiKey)))
130
+ throw new Error("Switcher API key is required.");
131
+ this.options = { ...options };
132
+ }
133
+ async request(method, path, body, options = {}) {
134
+ if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) || path.includes(".."))
135
+ throw new Error("Invalid API path.");
136
+ const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
137
+ if (!apiKey || /[\r\n]/.test(apiKey))
138
+ throw new Error("Switcher API key is required.");
139
+ const headers = { authorization: `Bearer ${apiKey}`, accept: "application/json" };
140
+ if (body !== undefined)
141
+ headers["content-type"] = "application/json";
142
+ if (method !== "GET")
143
+ headers["idempotency-key"] = options.idempotencyKey ?? crypto.randomUUID();
144
+ if (options.version !== undefined)
145
+ headers["if-match"] = String(options.version);
146
+ let response;
147
+ try {
148
+ response = await (this.options.fetch ?? fetch)(`${this.baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: "manual", signal: AbortSignal.timeout(this.options.timeoutMs ?? 120000) });
149
+ } catch {
150
+ throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
151
+ }
152
+ let data;
153
+ try {
154
+ data = await boundedJson(response);
155
+ } catch {
156
+ throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
157
+ }
158
+ if (!response.ok)
159
+ throw new SwitcherError(response.status, data?.error?.code ?? "api_error", data?.error?.message ?? `Switcher API returned HTTP ${response.status}.`, data?.error?.requestId);
160
+ return data;
161
+ }
162
+ query(options = {}) {
163
+ return new URLSearchParams(Object.entries(options).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString();
164
+ }
165
+ listProviders(options = {}) {
166
+ return this.request("GET", `/v1/providers?${this.query(options)}`);
167
+ }
168
+ getProvider(id) {
169
+ return this.request("GET", `/v1/providers/${encodeURIComponent(id)}`);
170
+ }
171
+ createProvider(input, idempotencyKey) {
172
+ return this.request("POST", "/v1/providers", input, { idempotencyKey });
173
+ }
174
+ updateProvider(input, version, idempotencyKey) {
175
+ return this.request("PUT", `/v1/providers/${encodeURIComponent(input.id)}`, input, { version, idempotencyKey });
176
+ }
177
+ deleteProvider(id, version, idempotencyKey) {
178
+ return this.request("DELETE", `/v1/providers/${encodeURIComponent(id)}`, undefined, { version, idempotencyKey });
179
+ }
180
+ refreshModels(id, idempotencyKey) {
181
+ return this.request("POST", `/v1/providers/${encodeURIComponent(id)}/refresh`, {}, { idempotencyKey });
182
+ }
183
+ listModels(id, options = {}) {
184
+ return this.request("GET", `/v1/providers/${encodeURIComponent(id)}/models?${this.query(options)}`);
185
+ }
186
+ listProfiles(options = {}) {
187
+ return this.request("GET", `/v1/profiles?${this.query(options)}`);
188
+ }
189
+ getProfile(id) {
190
+ return this.request("GET", `/v1/profiles/${encodeURIComponent(id)}`);
191
+ }
192
+ createProfile(input, idempotencyKey) {
193
+ return this.request("POST", "/v1/profiles", input, { idempotencyKey });
194
+ }
195
+ updateProfile(input, version, idempotencyKey) {
196
+ return this.request("PUT", `/v1/profiles/${encodeURIComponent(input.id)}`, input, { version, idempotencyKey });
197
+ }
198
+ deleteProfile(id, version, idempotencyKey) {
199
+ return this.request("DELETE", `/v1/profiles/${encodeURIComponent(id)}`, undefined, { version, idempotencyKey });
200
+ }
201
+ launchPlan(profileId, idempotencyKey) {
202
+ return this.request("POST", "/v1/launch-plans", { profileId }, { idempotencyKey });
203
+ }
204
+ listRuns(options = {}) {
205
+ return this.request("GET", `/v1/runs?${this.query(options)}`);
206
+ }
207
+ getRun(id) {
208
+ return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
209
+ }
210
+ createRun(input, idempotencyKey) {
211
+ return this.request("POST", "/v1/runs", input, { idempotencyKey });
212
+ }
213
+ finishRun(id, version, input, idempotencyKey) {
214
+ return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
215
+ }
216
+ }
217
+ function clientFromEnv(env = process.env) {
218
+ const credential = () => resolveCredential("switcher", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SWITCHER_API_KEY")), { keychain: { enabled: false } })?.apiKey ?? "";
219
+ if (!env.HASNA_SWITCHER_API_URL || !credential())
220
+ throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
221
+ return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
222
+ }
223
+ export {
224
+ clientFromEnv,
225
+ SwitcherError,
226
+ SwitcherClient
227
+ };
@@ -0,0 +1,9 @@
1
+ import { SwitcherClient } from "./sdk";
2
+ export declare function childEnvironment(env?: NodeJS.ProcessEnv): Record<string, string>;
3
+ export declare function launch(client: SwitcherClient, profileId: string, options?: {
4
+ cwd?: string;
5
+ executable?: string;
6
+ stateDir?: string;
7
+ args?: string[];
8
+ timeoutMs?: number;
9
+ }): Promise<number>;
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/mcp.ts
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z as z2 } from "zod";
8
+
9
+ // src/domain.ts
10
+ import { z } from "zod";
11
+ var VERSION = "0.1.0";
12
+ var harnessSchema = z.enum(["claude", "codex", "grok", "opencode2"]);
13
+ var protocolSchema = z.enum(["anthropic-messages", "openai-responses", "openai-chat"]);
14
+ var idSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/);
15
+ var label = z.string().min(1).max(200);
16
+ var envRef = z.string().regex(/^SWITCHER_PROVIDER_[A-Z0-9_]+$/);
17
+ function endpoint(value) {
18
+ let url;
19
+ try {
20
+ url = new URL(value);
21
+ } catch {
22
+ throw new Fault(400, "invalid_url", "Use an absolute HTTPS URL (HTTP is allowed on loopback).");
23
+ }
24
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
25
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local) || url.username || url.password || url.search || url.hash)
26
+ throw new Fault(400, "invalid_url", "URL must use HTTPS, contain no credentials/query/fragment, or use HTTP on loopback.");
27
+ return url.href.replace(/\/+$/, "");
28
+ }
29
+ var urlSchema = z.string().max(2000).superRefine((v, ctx) => {
30
+ try {
31
+ endpoint(v);
32
+ } catch {
33
+ ctx.addIssue({ code: "custom", message: "Invalid endpoint URL" });
34
+ }
35
+ }).transform(endpoint);
36
+ var modelSchema = z.object({
37
+ id: z.string().min(1).max(300),
38
+ name: label,
39
+ description: z.string().max(8000).optional(),
40
+ contextWindow: z.number().int().positive().optional(),
41
+ maxOutputTokens: z.number().int().positive().optional(),
42
+ inputModalities: z.array(z.string().max(50)).max(20).optional(),
43
+ outputModalities: z.array(z.string().max(50)).max(20).optional(),
44
+ supportedParameters: z.array(z.string().max(100)).max(100).optional()
45
+ }).strict();
46
+ var providerInputSchema = z.object({
47
+ id: idSchema,
48
+ name: label,
49
+ baseUrl: urlSchema,
50
+ protocol: protocolSchema,
51
+ credentialEnv: envRef.optional(),
52
+ authStyle: z.enum(["bearer", "x-api-key"]).default("bearer"),
53
+ modelsPath: z.string().regex(/^[a-zA-Z0-9_/-]+$/).max(200).default("models"),
54
+ manualModels: z.array(modelSchema).max(1e4).default([])
55
+ }).strict().refine((p) => !p.modelsPath.split("/").includes("..") && !p.modelsPath.startsWith("/"), "modelsPath must be relative");
56
+ var profileInputSchema = z.object({
57
+ id: idSchema,
58
+ name: label,
59
+ providerId: idSchema,
60
+ harness: harnessSchema,
61
+ model: z.string().min(1).max(300)
62
+ }).strict();
63
+ var runInputSchema = z.object({
64
+ profileId: idSchema,
65
+ harness: harnessSchema,
66
+ model: z.string().min(1).max(300),
67
+ planToken: z.string().regex(/^[a-f0-9]{64}$/)
68
+ }).strict();
69
+ var runUpdateSchema = z.object({
70
+ status: z.enum(["exited", "failed", "interrupted"]),
71
+ exitCode: z.number().int().min(0).max(255)
72
+ }).strict();
73
+
74
+ class Fault extends Error {
75
+ status;
76
+ code;
77
+ constructor(status, code, message) {
78
+ super(message);
79
+ this.status = status;
80
+ this.code = code;
81
+ }
82
+ }
83
+
84
+ // src/http.ts
85
+ var MAX_BYTES = 16 * 1024 * 1024;
86
+ async function boundedJson(response, maxBytes = MAX_BYTES) {
87
+ if (!response.body)
88
+ throw new Fault(502, "invalid_upstream", "Upstream returned no body.");
89
+ const reader = response.body.getReader();
90
+ const chunks = [];
91
+ let size = 0;
92
+ try {
93
+ while (true) {
94
+ const item = await reader.read();
95
+ if (item.done)
96
+ break;
97
+ size += item.value.byteLength;
98
+ if (size > maxBytes)
99
+ throw new Fault(502, "response_too_large", "Response exceeds the size limit.");
100
+ chunks.push(item.value);
101
+ }
102
+ const bytes = new Uint8Array(size);
103
+ let offset = 0;
104
+ for (const chunk of chunks) {
105
+ bytes.set(chunk, offset);
106
+ offset += chunk.length;
107
+ }
108
+ try {
109
+ return JSON.parse(new TextDecoder().decode(bytes));
110
+ } catch {
111
+ throw new Fault(502, "invalid_upstream", "Upstream returned invalid JSON.");
112
+ }
113
+ } finally {
114
+ await reader.cancel().catch(() => {});
115
+ }
116
+ }
117
+
118
+ // src/sdk.ts
119
+ import { resolveCredential } from "@hasna/contracts/client";
120
+
121
+ class SwitcherError extends Error {
122
+ status;
123
+ code;
124
+ requestId;
125
+ constructor(status, code, message, requestId) {
126
+ super(message);
127
+ this.status = status;
128
+ this.code = code;
129
+ this.requestId = requestId;
130
+ }
131
+ }
132
+
133
+ class SwitcherClient {
134
+ options;
135
+ baseUrl;
136
+ constructor(options) {
137
+ this.baseUrl = endpoint(options.baseUrl).replace(/\/v1$/, "");
138
+ if (typeof options.apiKey === "string" && (!options.apiKey || /[\r\n]/.test(options.apiKey)))
139
+ throw new Error("Switcher API key is required.");
140
+ this.options = { ...options };
141
+ }
142
+ async request(method, path, body, options = {}) {
143
+ if (!/^\/v1\/[a-zA-Z0-9/?&=._%+-]+$/.test(path) || path.includes(".."))
144
+ throw new Error("Invalid API path.");
145
+ const apiKey = typeof this.options.apiKey === "function" ? this.options.apiKey() : this.options.apiKey;
146
+ if (!apiKey || /[\r\n]/.test(apiKey))
147
+ throw new Error("Switcher API key is required.");
148
+ const headers = { authorization: `Bearer ${apiKey}`, accept: "application/json" };
149
+ if (body !== undefined)
150
+ headers["content-type"] = "application/json";
151
+ if (method !== "GET")
152
+ headers["idempotency-key"] = options.idempotencyKey ?? crypto.randomUUID();
153
+ if (options.version !== undefined)
154
+ headers["if-match"] = String(options.version);
155
+ let response;
156
+ try {
157
+ response = await (this.options.fetch ?? fetch)(`${this.baseUrl}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: "manual", signal: AbortSignal.timeout(this.options.timeoutMs ?? 120000) });
158
+ } catch {
159
+ throw new SwitcherError(0, "connection_failed", "Switcher API request failed; check endpoint and service availability.");
160
+ }
161
+ let data;
162
+ try {
163
+ data = await boundedJson(response);
164
+ } catch {
165
+ throw new SwitcherError(response.status, "invalid_response", "Switcher API returned invalid JSON.");
166
+ }
167
+ if (!response.ok)
168
+ throw new SwitcherError(response.status, data?.error?.code ?? "api_error", data?.error?.message ?? `Switcher API returned HTTP ${response.status}.`, data?.error?.requestId);
169
+ return data;
170
+ }
171
+ query(options = {}) {
172
+ return new URLSearchParams(Object.entries(options).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString();
173
+ }
174
+ listProviders(options = {}) {
175
+ return this.request("GET", `/v1/providers?${this.query(options)}`);
176
+ }
177
+ getProvider(id) {
178
+ return this.request("GET", `/v1/providers/${encodeURIComponent(id)}`);
179
+ }
180
+ createProvider(input, idempotencyKey) {
181
+ return this.request("POST", "/v1/providers", input, { idempotencyKey });
182
+ }
183
+ updateProvider(input, version, idempotencyKey) {
184
+ return this.request("PUT", `/v1/providers/${encodeURIComponent(input.id)}`, input, { version, idempotencyKey });
185
+ }
186
+ deleteProvider(id, version, idempotencyKey) {
187
+ return this.request("DELETE", `/v1/providers/${encodeURIComponent(id)}`, undefined, { version, idempotencyKey });
188
+ }
189
+ refreshModels(id, idempotencyKey) {
190
+ return this.request("POST", `/v1/providers/${encodeURIComponent(id)}/refresh`, {}, { idempotencyKey });
191
+ }
192
+ listModels(id, options = {}) {
193
+ return this.request("GET", `/v1/providers/${encodeURIComponent(id)}/models?${this.query(options)}`);
194
+ }
195
+ listProfiles(options = {}) {
196
+ return this.request("GET", `/v1/profiles?${this.query(options)}`);
197
+ }
198
+ getProfile(id) {
199
+ return this.request("GET", `/v1/profiles/${encodeURIComponent(id)}`);
200
+ }
201
+ createProfile(input, idempotencyKey) {
202
+ return this.request("POST", "/v1/profiles", input, { idempotencyKey });
203
+ }
204
+ updateProfile(input, version, idempotencyKey) {
205
+ return this.request("PUT", `/v1/profiles/${encodeURIComponent(input.id)}`, input, { version, idempotencyKey });
206
+ }
207
+ deleteProfile(id, version, idempotencyKey) {
208
+ return this.request("DELETE", `/v1/profiles/${encodeURIComponent(id)}`, undefined, { version, idempotencyKey });
209
+ }
210
+ launchPlan(profileId, idempotencyKey) {
211
+ return this.request("POST", "/v1/launch-plans", { profileId }, { idempotencyKey });
212
+ }
213
+ listRuns(options = {}) {
214
+ return this.request("GET", `/v1/runs?${this.query(options)}`);
215
+ }
216
+ getRun(id) {
217
+ return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
218
+ }
219
+ createRun(input, idempotencyKey) {
220
+ return this.request("POST", "/v1/runs", input, { idempotencyKey });
221
+ }
222
+ finishRun(id, version, input, idempotencyKey) {
223
+ return this.request("PATCH", `/v1/runs/${encodeURIComponent(id)}`, input, { version, idempotencyKey });
224
+ }
225
+ }
226
+ function clientFromEnv(env = process.env) {
227
+ const credential = () => resolveCredential("switcher", Object.fromEntries(Object.entries(env).filter(([name]) => name === "HASNA_SWITCHER_API_KEY")), { keychain: { enabled: false } })?.apiKey ?? "";
228
+ if (!env.HASNA_SWITCHER_API_URL || !credential())
229
+ throw new Error("Set HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY; no local database fallback is available.");
230
+ return new SwitcherClient({ baseUrl: env.HASNA_SWITCHER_API_URL, apiKey: credential });
231
+ }
232
+
233
+ // src/mcp.ts
234
+ var server = new McpServer({ name: "switcher", version: VERSION });
235
+ var page = { limit: z2.number().int().min(1).max(1000).optional(), offset: z2.number().int().nonnegative().optional(), search: z2.string().optional() };
236
+ function tool(name, description, schema, run) {
237
+ server.tool(name, description, schema, async (input) => {
238
+ try {
239
+ return { content: [{ type: "text", text: JSON.stringify(await run(input)) }] };
240
+ } catch {
241
+ return { isError: true, content: [{ type: "text", text: "Switcher API operation failed. Check configuration, input and API availability." }] };
242
+ }
243
+ });
244
+ }
245
+ tool("providers_list", "List provider profiles.", page, (p) => clientFromEnv().listProviders(p));
246
+ tool("providers_get", "Get a provider.", { id: z2.string() }, (p) => clientFromEnv().getProvider(p.id));
247
+ tool("providers_create", "Create a provider using credential environment references only.", providerInputSchema.innerType().shape, (p) => clientFromEnv().createProvider(p));
248
+ tool("providers_update", "Replace a provider at its current version.", { provider: providerInputSchema, version: z2.number().int() }, (p) => clientFromEnv().updateProvider(p.provider, p.version));
249
+ tool("providers_delete", "Delete an unreferenced provider.", { id: z2.string(), version: z2.number().int() }, (p) => clientFromEnv().deleteProvider(p.id, p.version));
250
+ tool("models_list", "List catalog with capability information.", { id: z2.string(), ...page }, (p) => {
251
+ const { id, ...rest } = p;
252
+ return clientFromEnv().listModels(id, rest);
253
+ });
254
+ tool("models_refresh", "Discover provider models.", { id: z2.string() }, (p) => clientFromEnv().refreshModels(p.id));
255
+ tool("profiles_list", "List harness launch profiles.", page, (p) => clientFromEnv().listProfiles(p));
256
+ tool("profiles_get", "Get a harness profile.", { id: z2.string() }, (p) => clientFromEnv().getProfile(p.id));
257
+ tool("profiles_create", "Create a harness launch profile.", profileInputSchema.shape, (p) => clientFromEnv().createProfile(p));
258
+ tool("profiles_update", "Replace a harness profile at its version.", { profile: profileInputSchema, version: z2.number().int() }, (p) => clientFromEnv().updateProfile(p.profile, p.version));
259
+ tool("profiles_delete", "Delete a profile without run history.", { id: z2.string(), version: z2.number().int() }, (p) => clientFromEnv().deleteProfile(p.id, p.version));
260
+ tool("launch_plan", "Validate a local launch plan; does not execute a remote process.", { profileId: z2.string() }, (p) => clientFromEnv().launchPlan(p.profileId));
261
+ tool("runs_list", "List launch metadata.", page, (p) => clientFromEnv().listRuns(p));
262
+ tool("runs_get", "Get launch metadata.", { id: z2.string() }, (p) => clientFromEnv().getRun(p.id));
263
+ if (process.argv.includes("--version"))
264
+ console.log(VERSION);
265
+ else if (process.argv.includes("--help"))
266
+ console.log("switcher-mcp: authenticated Switcher API tools over MCP stdio. Requires HASNA_SWITCHER_API_URL and HASNA_SWITCHER_API_KEY.");
267
+ else
268
+ await server.connect(new StdioServerTransport);
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};