@infrawise/mcp-server 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -13,27 +13,30 @@ Tools exposed to Claude Code:
13
13
 
14
14
  All tools support optional `subscription_filter` (subscription UUID).
15
15
 
16
- ## 5-minute setup
16
+ ## 2-step quickstart
17
17
 
18
18
  ```bash
19
- # 1) Authenticate once with Azure AD device code flow
20
- npx @infrawise/mcp-server auth
21
- ```
19
+ # 1) Authenticate + register + validate in one command
20
+ npx @infrawise/mcp-server setup
22
21
 
23
- Add this to `~/.claude/settings.json`:
24
-
25
- ```json
26
- {
27
- "mcpServers": {
28
- "infrawise": {
29
- "command": "npx",
30
- "args": ["@infrawise/mcp-server"]
31
- }
32
- }
33
- }
22
+ # 2) Restart Claude Code
34
23
  ```
35
24
 
36
- Restart Claude Code.
25
+ > **Note:** Use `claude mcp add` — do not manually add `mcpServers` to `~/.claude/settings.json`, that key is not valid there.
26
+
27
+ ## Helpful commands
28
+
29
+ ```bash
30
+ # Authenticate only (supports --tenant)
31
+ npx @infrawise/mcp-server auth
32
+ npx @infrawise/mcp-server auth --tenant <tenantId>
33
+
34
+ # Setup with explicit tenant
35
+ npx @infrawise/mcp-server setup --tenant <tenantId>
36
+
37
+ # Diagnose auth and onboarding readiness with fix steps
38
+ npx @infrawise/mcp-server doctor
39
+ ```
37
40
 
38
41
  ## Local development
39
42
 
@@ -44,12 +47,26 @@ npm run build
44
47
  node dist/index.js auth
45
48
  ```
46
49
 
50
+ ## Deploying package updates (npm publish)
51
+
52
+ ```bash
53
+ cd packages/claude-mcp
54
+ npm publish --access public
55
+ ```
56
+
57
+ Notes:
58
+
59
+ - `prepublishOnly` automatically runs `npm run clean && npm run build`
60
+ - If npm prompts for browser sign-in or passkey/2FA approval, complete that flow and rerun publish if needed
61
+ - Verify deployment with: `npm view @infrawise/mcp-server version`
62
+
47
63
  ## Environment variables
48
64
 
49
65
  - `INFRAWISE_API_BASE` (default: `https://api.infrawiseai.com/api`)
50
66
  - `INFRAWISE_AZURE_CLIENT_ID` (default: Infrawise API app ID)
51
67
  - `INFRAWISE_AZURE_API_SCOPE` (default: `api://<clientId>/delegated_access`)
52
68
  - `INFRAWISE_AZURE_AUTHORITY` (default: `https://login.microsoftonline.com/common`)
69
+ - `INFRAWISE_AZURE_TENANT_ID` (optional: your Azure AD tenant ID; when set, authority is built automatically)
53
70
 
54
71
  ## Security
55
72
 
package/dist/auth.d.ts CHANGED
@@ -5,6 +5,21 @@ export interface AuthSettings {
5
5
  authority: string;
6
6
  clientId: string;
7
7
  scope: string;
8
+ tenantId?: string;
8
9
  }
9
- export declare function runDeviceCodeAuth(): Promise<void>;
10
- export declare function getAccessToken(): Promise<string>;
10
+ export interface AuthCommandOptions {
11
+ tenantId?: string;
12
+ }
13
+ export interface TokenCacheDiagnostics {
14
+ credentialPath: string;
15
+ cacheExists: boolean;
16
+ cacheUpdatedAt?: string;
17
+ cacheAuthority?: string;
18
+ resolvedAuthority: string;
19
+ resolvedTenantId?: string;
20
+ clientId: string;
21
+ scope: string;
22
+ }
23
+ export declare function runDeviceCodeAuth(options?: AuthCommandOptions): Promise<void>;
24
+ export declare function getTokenCacheDiagnostics(options?: AuthCommandOptions): Promise<TokenCacheDiagnostics>;
25
+ export declare function getAccessToken(options?: AuthCommandOptions): Promise<string>;
package/dist/auth.js CHANGED
@@ -3,23 +3,53 @@ import { promises as fs } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  const DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common";
6
+ const ORGANIZATIONS_AUTHORITY = "https://login.microsoftonline.com/organizations";
7
+ const MSA_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad";
8
+ const DEVICE_CODE_AUTH_TIMEOUT_MS = 120_000;
6
9
  const DEFAULT_API_CLIENT_ID = "06dc6d06-11f1-4543-bb2c-9c91b263df56";
7
10
  const DEFAULT_SCOPE = `api://${DEFAULT_API_CLIENT_ID}/delegated_access`;
8
11
  const CREDENTIALS_DIR = ".infrawise";
9
12
  const CREDENTIALS_FILE = "mcp-credentials.json";
10
13
  export class NotAuthenticatedError extends Error {
11
- constructor(message = "Not authenticated. Run: npx @infrawise/mcp-server auth") {
14
+ constructor(message = "Not authenticated with Azure AD. Run: npx @infrawise/mcp-server auth") {
12
15
  super(message);
13
16
  this.name = "NotAuthenticatedError";
14
17
  }
15
18
  }
16
- function getAuthSettings() {
17
- const clientId = process.env.INFRAWISE_AZURE_CLIENT_ID ?? DEFAULT_API_CLIENT_ID;
18
- const scope = process.env.INFRAWISE_AZURE_API_SCOPE ?? `api://${clientId}/delegated_access`;
19
+ class AuthTimeoutError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "AuthTimeoutError";
23
+ }
24
+ }
25
+ function normalizeTenantId(raw) {
26
+ const tenantId = raw?.trim();
27
+ return tenantId ? tenantId : undefined;
28
+ }
29
+ function buildTenantAuthority(tenantId) {
30
+ return `https://login.microsoftonline.com/${tenantId}`;
31
+ }
32
+ function getConfiguredAuthority() {
33
+ const value = process.env.INFRAWISE_AZURE_AUTHORITY?.trim();
34
+ return value ? value : undefined;
35
+ }
36
+ function resolveTenantOverride(options) {
37
+ return normalizeTenantId(options?.tenantId) ?? normalizeTenantId(process.env.INFRAWISE_AZURE_TENANT_ID);
38
+ }
39
+ function getAuthSettings(payload, options, authorityOverride) {
40
+ const clientId = process.env.INFRAWISE_AZURE_CLIENT_ID ?? payload?.clientId ?? DEFAULT_API_CLIENT_ID;
41
+ const scope = process.env.INFRAWISE_AZURE_API_SCOPE ?? payload?.scope ?? `api://${clientId}/delegated_access`;
42
+ const tenantId = resolveTenantOverride(options);
43
+ const authority = authorityOverride ??
44
+ (tenantId ? buildTenantAuthority(tenantId) : undefined) ??
45
+ getConfiguredAuthority() ??
46
+ payload?.authority ??
47
+ DEFAULT_AUTHORITY;
19
48
  return {
20
- authority: process.env.INFRAWISE_AZURE_AUTHORITY ?? DEFAULT_AUTHORITY,
49
+ authority,
21
50
  clientId,
22
51
  scope,
52
+ tenantId,
23
53
  };
24
54
  }
25
55
  function getCredentialFilePath() {
@@ -58,8 +88,8 @@ async function writeCachePayload(payload) {
58
88
  // Best-effort on Windows.
59
89
  }
60
90
  }
61
- async function createClientFromSettings(settings) {
62
- const payload = await readCachePayload();
91
+ async function createClientFromSettings(settings, existingPayload) {
92
+ const payload = existingPayload ?? (await readCachePayload());
63
93
  const client = new PublicClientApplication({
64
94
  auth: {
65
95
  clientId: settings.clientId,
@@ -92,9 +122,37 @@ async function persistClientCache(client, settings, account) {
92
122
  updatedAt: new Date().toISOString(),
93
123
  });
94
124
  }
95
- export async function runDeviceCodeAuth() {
96
- const settings = getAuthSettings();
97
- const { client } = await createClientFromSettings(settings);
125
+ function toErrorMessage(error) {
126
+ return error instanceof Error ? error.message : String(error);
127
+ }
128
+ function getAuthorityAttempts(options) {
129
+ const tenantId = resolveTenantOverride(options);
130
+ if (tenantId) {
131
+ return [{ authority: buildTenantAuthority(tenantId), label: `tenant ${tenantId}` }];
132
+ }
133
+ const configuredAuthority = getConfiguredAuthority();
134
+ if (configuredAuthority) {
135
+ return [{ authority: configuredAuthority, label: "configured authority" }];
136
+ }
137
+ return [
138
+ { authority: DEFAULT_AUTHORITY, label: "common" },
139
+ { authority: ORGANIZATIONS_AUTHORITY, label: "organizations" },
140
+ ];
141
+ }
142
+ function readTenantIdFromResult(result) {
143
+ const claims = result.idTokenClaims;
144
+ return typeof claims?.tid === "string" ? claims.tid : undefined;
145
+ }
146
+ function assertTenantCompatibility(result, settings) {
147
+ const signedInTenantId = readTenantIdFromResult(result);
148
+ if (signedInTenantId?.toLowerCase() === MSA_TENANT_ID) {
149
+ throw new Error("Signed in with a personal Microsoft account. Sign in with your organization Azure account and run: npx @infrawise/mcp-server auth");
150
+ }
151
+ if (settings.tenantId && signedInTenantId && settings.tenantId.toLowerCase() !== signedInTenantId.toLowerCase()) {
152
+ throw new Error(`Account/tenant mismatch detected. You requested tenant ${settings.tenantId}, but authenticated into tenant ${signedInTenantId}. Re-run with the correct tenant: npx @infrawise/mcp-server auth --tenant <tenantId>`);
153
+ }
154
+ }
155
+ async function acquireTokenByDeviceCodeWithTimeout(client, settings) {
98
156
  const request = {
99
157
  scopes: [settings.scope, "openid", "profile"],
100
158
  deviceCodeCallback: (response) => {
@@ -103,16 +161,82 @@ export async function runDeviceCodeAuth() {
103
161
  console.error(`[Infrawise MCP] ${msg}`);
104
162
  },
105
163
  };
106
- const result = await client.acquireTokenByDeviceCode(request);
107
- if (!result || !result.account) {
108
- throw new Error("Device Code authentication did not return an account.");
164
+ let timeout;
165
+ try {
166
+ const authPromise = client.acquireTokenByDeviceCode(request);
167
+ const timeoutPromise = new Promise((_, reject) => {
168
+ timeout = setTimeout(() => {
169
+ reject(new AuthTimeoutError("Authentication did not complete within 120 seconds. Complete sign-in quickly after the device code is shown, or rerun with a specific tenant: npx @infrawise/mcp-server auth --tenant <tenantId>"));
170
+ }, DEVICE_CODE_AUTH_TIMEOUT_MS);
171
+ });
172
+ const result = await Promise.race([authPromise, timeoutPromise]);
173
+ if (!result) {
174
+ throw new Error("Device Code authentication did not return a result.");
175
+ }
176
+ return result;
109
177
  }
110
- await persistClientCache(client, settings, result.account);
111
- console.error(`[Infrawise MCP] Authentication complete. Token cache saved to ${getCredentialFilePath()}`);
178
+ finally {
179
+ if (timeout) {
180
+ clearTimeout(timeout);
181
+ }
182
+ }
183
+ }
184
+ export async function runDeviceCodeAuth(options) {
185
+ const payload = await readCachePayload();
186
+ const attempts = getAuthorityAttempts(options);
187
+ const errors = [];
188
+ for (const attempt of attempts) {
189
+ const settings = getAuthSettings(payload, options, attempt.authority);
190
+ const { client } = await createClientFromSettings(settings, payload);
191
+ try {
192
+ if (attempts.length > 1) {
193
+ console.error(`[Infrawise MCP] Authenticating with ${attempt.label} authority...`);
194
+ }
195
+ const result = await acquireTokenByDeviceCodeWithTimeout(client, settings);
196
+ if (!result.account) {
197
+ throw new Error("Device Code authentication did not return an account.");
198
+ }
199
+ assertTenantCompatibility(result, settings);
200
+ await persistClientCache(client, settings, result.account);
201
+ console.error(`[Infrawise MCP] Authentication complete. Token cache saved to ${getCredentialFilePath()}`);
202
+ return;
203
+ }
204
+ catch (error) {
205
+ errors.push(`${attempt.label}: ${toErrorMessage(error)}`);
206
+ }
207
+ }
208
+ const tenantHint = !resolveTenantOverride(options) && !getConfiguredAuthority()
209
+ ? " Hint: retry with your tenant ID: npx @infrawise/mcp-server auth --tenant <tenantId>."
210
+ : "";
211
+ throw new Error(`Authentication failed after trying available authorities (${errors.join(" | ")}).${tenantHint}`);
212
+ }
213
+ export async function getTokenCacheDiagnostics(options) {
214
+ const payload = await readCachePayload();
215
+ const settings = getAuthSettings(payload, options);
216
+ const credentialPath = getCredentialFilePath();
217
+ let cacheExists = false;
218
+ try {
219
+ await fs.access(credentialPath);
220
+ cacheExists = true;
221
+ }
222
+ catch {
223
+ cacheExists = false;
224
+ }
225
+ return {
226
+ credentialPath,
227
+ cacheExists,
228
+ cacheUpdatedAt: payload?.updatedAt,
229
+ cacheAuthority: payload?.authority,
230
+ resolvedAuthority: settings.authority,
231
+ resolvedTenantId: settings.tenantId,
232
+ clientId: settings.clientId,
233
+ scope: settings.scope,
234
+ };
112
235
  }
113
- export async function getAccessToken() {
114
- const settings = getAuthSettings();
115
- const { client, payload } = await createClientFromSettings(settings);
236
+ export async function getAccessToken(options) {
237
+ const payload = await readCachePayload();
238
+ const settings = getAuthSettings(payload, options);
239
+ const { client } = await createClientFromSettings(settings, payload);
116
240
  const account = await getPreferredAccount(client, payload?.homeAccountId);
117
241
  if (!account) {
118
242
  throw new NotAuthenticatedError();
package/dist/client.d.ts CHANGED
@@ -1,7 +1,27 @@
1
+ type OnboardingState = "ACTIVE" | "PENDING_DELEGATION" | "EXPIRED" | "SUSPENDED" | "UNKNOWN";
2
+ export interface TenantReadinessEndpointResult {
3
+ ok: boolean;
4
+ status?: number;
5
+ code?: InfrawiseErrorCode;
6
+ message: string;
7
+ }
8
+ export interface TenantReadinessReport {
9
+ tenantValidation: TenantReadinessEndpointResult;
10
+ onboardingStatus: TenantReadinessEndpointResult & {
11
+ states?: OnboardingState[];
12
+ };
13
+ }
1
14
  export type InfrawiseErrorCode = "NOT_AUTHENTICATED" | "NOT_ONBOARDED" | "PENDING_DELEGATION" | "UPSTREAM_ERROR";
15
+ export interface InfrawiseRequestOptions {
16
+ accessToken?: string;
17
+ skipTenantCheck?: boolean;
18
+ }
2
19
  export declare class InfrawiseClientError extends Error {
3
20
  code: InfrawiseErrorCode;
4
21
  status?: number;
5
22
  constructor(code: InfrawiseErrorCode, message: string, status?: number);
6
23
  }
7
- export declare function getInfrawiseData<T>(path: string): Promise<T>;
24
+ export declare function ensureTenantReady(): Promise<string>;
25
+ export declare function getTenantReadinessReport(token: string): Promise<TenantReadinessReport>;
26
+ export declare function getInfrawiseData<T>(path: string, options?: InfrawiseRequestOptions): Promise<T>;
27
+ export {};
package/dist/client.js CHANGED
@@ -32,7 +32,7 @@ async function fetchJson(path, token) {
32
32
  signal: controller.signal,
33
33
  });
34
34
  if (response.status === 401 || response.status === 403) {
35
- throw new InfrawiseClientError("NOT_AUTHENTICATED", "Token rejected by Infrawise API. Re-authenticate with: npx @infrawise/mcp-server auth", response.status);
35
+ throw new InfrawiseClientError("NOT_AUTHENTICATED", "Azure AD token rejected by Infrawise API. Re-authenticate with: npx @infrawise/mcp-server auth", response.status);
36
36
  }
37
37
  if (!response.ok) {
38
38
  const body = await response.text();
@@ -66,6 +66,20 @@ function normalizeOnboardingState(status) {
66
66
  }
67
67
  return "UNKNOWN";
68
68
  }
69
+ function toEndpointResult(error, fallbackMessage) {
70
+ if (error instanceof InfrawiseClientError) {
71
+ return {
72
+ ok: false,
73
+ status: error.status,
74
+ code: error.code,
75
+ message: error.message,
76
+ };
77
+ }
78
+ return {
79
+ ok: false,
80
+ message: fallbackMessage,
81
+ };
82
+ }
69
83
  async function assertTenantReady(token) {
70
84
  const tenantValidation = await fetchJson("/auth/validate-tenant", token);
71
85
  if (!tenantValidation.data?.authorized) {
@@ -85,10 +99,9 @@ async function assertTenantReady(token) {
85
99
  }
86
100
  throw new InfrawiseClientError("NOT_ONBOARDED", "No active Infrawise subscription found. Complete onboarding at https://infrawiseai.com/onboard");
87
101
  }
88
- export async function getInfrawiseData(path) {
89
- let token;
102
+ async function resolveAccessToken() {
90
103
  try {
91
- token = await getAccessToken();
104
+ return await getAccessToken();
92
105
  }
93
106
  catch (error) {
94
107
  if (error instanceof NotAuthenticatedError) {
@@ -96,7 +109,91 @@ export async function getInfrawiseData(path) {
96
109
  }
97
110
  throw error;
98
111
  }
112
+ }
113
+ export async function ensureTenantReady() {
114
+ const token = await resolveAccessToken();
99
115
  await assertTenantReady(token);
116
+ return token;
117
+ }
118
+ export async function getTenantReadinessReport(token) {
119
+ let tenantValidation;
120
+ try {
121
+ const validation = await fetchJson("/auth/validate-tenant", token);
122
+ if (validation.data?.authorized) {
123
+ tenantValidation = {
124
+ ok: true,
125
+ status: validation.status,
126
+ message: "Tenant is authorized.",
127
+ };
128
+ }
129
+ else {
130
+ tenantValidation = {
131
+ ok: false,
132
+ status: validation.status,
133
+ code: "NOT_ONBOARDED",
134
+ message: validation.data?.message ?? "Tenant is not authorized in Infrawise.",
135
+ };
136
+ }
137
+ }
138
+ catch (error) {
139
+ tenantValidation = toEndpointResult(error, "Unable to validate tenant authorization.");
140
+ }
141
+ let onboardingStatus;
142
+ try {
143
+ const onboarding = await fetchJson("/onboarding/status", token);
144
+ if (!Array.isArray(onboarding.data) || onboarding.data.length === 0) {
145
+ onboardingStatus = {
146
+ ok: true,
147
+ status: onboarding.status,
148
+ message: "No onboarding records found (legacy allowlisted tenant).",
149
+ };
150
+ }
151
+ else {
152
+ const states = onboarding.data.map((entry) => normalizeOnboardingState(entry.status));
153
+ if (states.includes("ACTIVE")) {
154
+ onboardingStatus = {
155
+ ok: true,
156
+ status: onboarding.status,
157
+ states,
158
+ message: "At least one onboarding record is ACTIVE.",
159
+ };
160
+ }
161
+ else if (states.includes("PENDING_DELEGATION")) {
162
+ onboardingStatus = {
163
+ ok: false,
164
+ status: onboarding.status,
165
+ code: "PENDING_DELEGATION",
166
+ states,
167
+ message: "Onboarding is pending Lighthouse delegation.",
168
+ };
169
+ }
170
+ else {
171
+ onboardingStatus = {
172
+ ok: false,
173
+ status: onboarding.status,
174
+ code: "NOT_ONBOARDED",
175
+ states,
176
+ message: "No ACTIVE onboarding records found.",
177
+ };
178
+ }
179
+ }
180
+ }
181
+ catch (error) {
182
+ onboardingStatus = {
183
+ ...toEndpointResult(error, "Unable to read onboarding status."),
184
+ states: undefined,
185
+ };
186
+ }
187
+ return {
188
+ tenantValidation,
189
+ onboardingStatus,
190
+ };
191
+ }
192
+ export async function getInfrawiseData(path, options = {}) {
193
+ const token = options.accessToken ?? (await resolveAccessToken());
194
+ if (!options.skipTenantCheck) {
195
+ await assertTenantReady(token);
196
+ }
100
197
  const response = await fetchJson(path, token);
101
198
  return response.data;
102
199
  }
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
5
- import { runDeviceCodeAuth } from "./auth.js";
6
- import { InfrawiseClientError } from "./client.js";
6
+ import { getAccessToken, getTokenCacheDiagnostics, runDeviceCodeAuth } from "./auth.js";
7
+ import { getTenantReadinessReport, InfrawiseClientError } from "./client.js";
7
8
  import { getGeneralRecommendations } from "./tools/general-recommendations.js";
8
9
  import { getIdleResources } from "./tools/idle-resources.js";
9
10
  import { getSavingsSummary } from "./tools/summary.js";
@@ -47,15 +48,173 @@ function toToolError(error) {
47
48
  ],
48
49
  };
49
50
  }
50
- async function runAuthMode() {
51
+ function parseCliOptions(argv) {
52
+ const firstArg = argv[2];
53
+ const mode = firstArg === "auth" || firstArg === "setup" || firstArg === "doctor" ? firstArg : "server";
54
+ const optionStart = mode === "server" ? 2 : 3;
55
+ let tenantId;
56
+ for (let i = optionStart; i < argv.length; i += 1) {
57
+ const arg = argv[i];
58
+ if (arg === "--tenant") {
59
+ const next = argv[i + 1];
60
+ if (!next || next.startsWith("--")) {
61
+ throw new Error("Missing value for --tenant. Usage: --tenant <tenantId>");
62
+ }
63
+ tenantId = next;
64
+ i += 1;
65
+ continue;
66
+ }
67
+ if (arg.startsWith("--tenant=")) {
68
+ tenantId = arg.slice("--tenant=".length);
69
+ continue;
70
+ }
71
+ throw new Error(`Unknown argument: ${arg}`);
72
+ }
73
+ return { mode, tenantId };
74
+ }
75
+ function commandWithTenant(base, tenantId) {
76
+ return tenantId ? `${base} --tenant ${tenantId}` : base;
77
+ }
78
+ function getClaudeCommand() {
79
+ return process.platform === "win32" ? "claude.cmd" : "claude";
80
+ }
81
+ async function runCommand(command, args) {
82
+ return new Promise((resolve, reject) => {
83
+ const child = spawn(command, args, {
84
+ stdio: ["ignore", "pipe", "pipe"],
85
+ windowsHide: true,
86
+ });
87
+ let stdout = "";
88
+ let stderr = "";
89
+ child.stdout.on("data", (chunk) => {
90
+ stdout += String(chunk);
91
+ });
92
+ child.stderr.on("data", (chunk) => {
93
+ stderr += String(chunk);
94
+ });
95
+ child.on("error", (error) => {
96
+ if (error.code === "ENOENT") {
97
+ reject(new Error("Claude Code CLI is not installed or not on PATH. Install Claude Code, then retry setup."));
98
+ return;
99
+ }
100
+ reject(error);
101
+ });
102
+ child.on("close", (code) => {
103
+ resolve({ code: code ?? 1, stdout, stderr });
104
+ });
105
+ });
106
+ }
107
+ function isInfrawiseRegistered(listOutput) {
108
+ return /\binfrawise\b/i.test(listOutput);
109
+ }
110
+ async function listMcpRegistrations() {
111
+ const result = await runCommand(getClaudeCommand(), ["mcp", "list"]);
112
+ if (result.code !== 0) {
113
+ throw new Error(`Failed to list MCP servers: ${result.stderr || result.stdout || "unknown error"}`);
114
+ }
115
+ return result.stdout;
116
+ }
117
+ async function addMcpRegistration() {
118
+ const args = ["mcp", "add", "infrawise", "--", "npx", "@infrawise/mcp-server"];
119
+ const result = await runCommand(getClaudeCommand(), args);
120
+ if (result.code !== 0) {
121
+ throw new Error(`Failed to add MCP server: ${result.stderr || result.stdout || "unknown error"}`);
122
+ }
123
+ }
124
+ async function runAuthMode(options) {
125
+ await runDeviceCodeAuth({ tenantId: options.tenantId });
126
+ }
127
+ async function runSetupMode(options) {
128
+ await runDeviceCodeAuth({ tenantId: options.tenantId });
129
+ const before = await listMcpRegistrations();
130
+ if (!isInfrawiseRegistered(before)) {
131
+ await addMcpRegistration();
132
+ }
133
+ const after = await listMcpRegistrations();
134
+ if (!isInfrawiseRegistered(after)) {
135
+ throw new Error("Setup could not verify the Infrawise MCP registration. Run: claude mcp add infrawise -- npx @infrawise/mcp-server");
136
+ }
137
+ console.error("[Infrawise MCP] READY: Authenticated and registered with Claude Code. Restart Claude Code to load the server.");
138
+ }
139
+ function printCheck(ok, label, detail) {
140
+ const status = ok ? "PASS" : "FAIL";
141
+ console.error(`[Infrawise MCP Doctor] ${status} - ${label}: ${detail}`);
142
+ }
143
+ function addFix(fixes, fix) {
144
+ if (!fixes.includes(fix)) {
145
+ fixes.push(fix);
146
+ }
147
+ }
148
+ async function runDoctorMode(options) {
149
+ const cache = await getTokenCacheDiagnostics({ tenantId: options.tenantId });
150
+ const fixes = [];
151
+ const authCommand = commandWithTenant("npx @infrawise/mcp-server auth", options.tenantId);
152
+ printCheck(cache.cacheExists, "Token cache", cache.cacheExists ? `Found at ${cache.credentialPath}` : `Missing at ${cache.credentialPath}`);
153
+ if (!cache.cacheExists) {
154
+ addFix(fixes, `Authenticate: ${authCommand}`);
155
+ }
156
+ const authorityMatchesCache = !cache.cacheAuthority || cache.cacheAuthority === cache.resolvedAuthority;
157
+ printCheck(authorityMatchesCache, "Authority", `Resolved=${cache.resolvedAuthority}${cache.cacheAuthority ? `, cached=${cache.cacheAuthority}` : ""}`);
158
+ if (!authorityMatchesCache) {
159
+ addFix(fixes, `Re-authenticate to refresh authority metadata: ${authCommand}`);
160
+ }
161
+ let token;
51
162
  try {
52
- await runDeviceCodeAuth();
163
+ token = await getAccessToken({ tenantId: options.tenantId });
164
+ printCheck(true, "Silent token acquisition", "Access token acquired successfully.");
53
165
  }
54
166
  catch (error) {
55
167
  const message = error instanceof Error ? error.message : String(error);
56
- console.error(`[Infrawise MCP] Authentication failed: ${message}`);
168
+ printCheck(false, "Silent token acquisition", message);
169
+ addFix(fixes, `Re-authenticate: ${authCommand}`);
170
+ }
171
+ if (!token) {
172
+ console.error("[Infrawise MCP Doctor] Not ready.");
173
+ if (fixes.length > 0) {
174
+ console.error("[Infrawise MCP Doctor] Fix steps:");
175
+ fixes.forEach((fix, index) => {
176
+ console.error(`${index + 1}. ${fix}`);
177
+ });
178
+ }
57
179
  process.exitCode = 1;
180
+ return;
181
+ }
182
+ const readiness = await getTenantReadinessReport(token);
183
+ printCheck(readiness.tenantValidation.ok, "Tenant validation (/auth/validate-tenant)", readiness.tenantValidation.message);
184
+ if (!readiness.tenantValidation.ok) {
185
+ if (readiness.tenantValidation.code === "NOT_AUTHENTICATED") {
186
+ addFix(fixes, `Re-authenticate: ${authCommand}`);
187
+ }
188
+ else {
189
+ addFix(fixes, "Complete onboarding: https://infrawiseai.com/onboard");
190
+ }
191
+ }
192
+ const onboardingDetail = readiness.onboardingStatus.states?.length
193
+ ? `${readiness.onboardingStatus.message} States=${readiness.onboardingStatus.states.join(",")}`
194
+ : readiness.onboardingStatus.message;
195
+ printCheck(readiness.onboardingStatus.ok, "Onboarding status (/onboarding/status)", onboardingDetail);
196
+ if (!readiness.onboardingStatus.ok) {
197
+ if (readiness.onboardingStatus.code === "PENDING_DELEGATION") {
198
+ addFix(fixes, "Finish Lighthouse delegation in onboarding: https://infrawiseai.com/onboard");
199
+ }
200
+ else if (readiness.onboardingStatus.code === "NOT_AUTHENTICATED") {
201
+ addFix(fixes, `Re-authenticate: ${authCommand}`);
202
+ }
203
+ else {
204
+ addFix(fixes, "Ensure at least one ACTIVE Infrawise onboarding record: https://infrawiseai.com/onboard");
205
+ }
206
+ }
207
+ const ready = fixes.length === 0;
208
+ if (ready) {
209
+ console.error("[Infrawise MCP Doctor] READY: All checks passed.");
210
+ return;
58
211
  }
212
+ console.error("[Infrawise MCP Doctor] Not ready.");
213
+ console.error("[Infrawise MCP Doctor] Fix steps:");
214
+ fixes.forEach((fix, index) => {
215
+ console.error(`${index + 1}. ${fix}`);
216
+ });
217
+ process.exitCode = 1;
59
218
  }
60
219
  async function runServerMode() {
61
220
  const server = new McpServer({
@@ -109,11 +268,26 @@ async function runServerMode() {
109
268
  await server.connect(transport);
110
269
  }
111
270
  async function main() {
112
- const mode = process.argv[2];
113
- if (mode === "auth") {
114
- await runAuthMode();
115
- return;
271
+ try {
272
+ const options = parseCliOptions(process.argv);
273
+ if (options.mode === "auth") {
274
+ await runAuthMode(options);
275
+ return;
276
+ }
277
+ if (options.mode === "setup") {
278
+ await runSetupMode(options);
279
+ return;
280
+ }
281
+ if (options.mode === "doctor") {
282
+ await runDoctorMode(options);
283
+ return;
284
+ }
285
+ await runServerMode();
286
+ }
287
+ catch (error) {
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ console.error(`[Infrawise MCP] ${message}`);
290
+ process.exitCode = 1;
116
291
  }
117
- await runServerMode();
118
292
  }
119
293
  await main();
@@ -1,2 +1,7 @@
1
1
  import { type GeneralRecommendation } from "./types.js";
2
- export declare function getGeneralRecommendations(subscriptionFilter?: string): Promise<GeneralRecommendation[]>;
2
+ interface ToolRequestOptions {
3
+ accessToken?: string;
4
+ skipTenantCheck?: boolean;
5
+ }
6
+ export declare function getGeneralRecommendations(subscriptionFilter?: string, options?: ToolRequestOptions): Promise<GeneralRecommendation[]>;
7
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { getInfrawiseData } from "../client.js";
2
2
  import { filterBySubscription } from "./types.js";
3
- export async function getGeneralRecommendations(subscriptionFilter) {
4
- const data = await getInfrawiseData("/general-recommendations");
3
+ export async function getGeneralRecommendations(subscriptionFilter, options = {}) {
4
+ const data = await getInfrawiseData("/general-recommendations", options);
5
5
  const rows = Array.isArray(data) ? data : [];
6
6
  return filterBySubscription(rows, subscriptionFilter);
7
7
  }
@@ -1,2 +1,7 @@
1
1
  import { type IdleResource } from "./types.js";
2
- export declare function getIdleResources(subscriptionFilter?: string): Promise<IdleResource[]>;
2
+ interface ToolRequestOptions {
3
+ accessToken?: string;
4
+ skipTenantCheck?: boolean;
5
+ }
6
+ export declare function getIdleResources(subscriptionFilter?: string, options?: ToolRequestOptions): Promise<IdleResource[]>;
7
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { getInfrawiseData } from "../client.js";
2
2
  import { filterBySubscription } from "./types.js";
3
- export async function getIdleResources(subscriptionFilter) {
4
- const data = await getInfrawiseData("/idle-resources");
3
+ export async function getIdleResources(subscriptionFilter, options = {}) {
4
+ const data = await getInfrawiseData("/idle-resources", options);
5
5
  const rows = Array.isArray(data) ? data : [];
6
6
  return filterBySubscription(rows, subscriptionFilter);
7
7
  }
@@ -1,2 +1,7 @@
1
1
  import { type SkuOptimization } from "./types.js";
2
- export declare function getSkuOptimizations(subscriptionFilter?: string): Promise<SkuOptimization[]>;
2
+ interface ToolRequestOptions {
3
+ accessToken?: string;
4
+ skipTenantCheck?: boolean;
5
+ }
6
+ export declare function getSkuOptimizations(subscriptionFilter?: string, options?: ToolRequestOptions): Promise<SkuOptimization[]>;
7
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { getInfrawiseData } from "../client.js";
2
2
  import { filterBySubscription } from "./types.js";
3
- export async function getSkuOptimizations(subscriptionFilter) {
4
- const data = await getInfrawiseData("/sku-optimizations");
3
+ export async function getSkuOptimizations(subscriptionFilter, options = {}) {
4
+ const data = await getInfrawiseData("/sku-optimizations", options);
5
5
  const rows = Array.isArray(data) ? data : [];
6
6
  return filterBySubscription(rows, subscriptionFilter);
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { ensureTenantReady } from "../client.js";
1
2
  import { getGeneralRecommendations } from "./general-recommendations.js";
2
3
  import { getIdleResources } from "./idle-resources.js";
3
4
  import { getSkuOptimizations } from "./sku-optimizations.js";
@@ -6,10 +7,11 @@ function roundCurrency(value) {
6
7
  return Math.round(value * 100) / 100;
7
8
  }
8
9
  export async function getSavingsSummary(subscriptionFilter) {
10
+ const accessToken = await ensureTenantReady();
9
11
  const [idleResources, skuOptimizations, generalRecommendations] = await Promise.all([
10
- getIdleResources(subscriptionFilter),
11
- getSkuOptimizations(subscriptionFilter),
12
- getGeneralRecommendations(subscriptionFilter),
12
+ getIdleResources(subscriptionFilter, { accessToken, skipTenantCheck: true }),
13
+ getSkuOptimizations(subscriptionFilter, { accessToken, skipTenantCheck: true }),
14
+ getGeneralRecommendations(subscriptionFilter, { accessToken, skipTenantCheck: true }),
13
15
  ]);
14
16
  const idleMonthly = idleResources.reduce((sum, item) => sum + toNumber(item.monthlyCost), 0);
15
17
  const idleAnnual = idleResources.reduce((sum, item) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infrawise/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Infrawise MCP server for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/admonitor-inc/infrawise.git",
17
+ "url": "git+https://github.com/Infrawise/infrawise.git",
18
18
  "directory": "packages/claude-mcp"
19
19
  },
20
20
  "scripts": {