@infrawise/mcp-server 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.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @infrawise/mcp-server
2
+
3
+ Claude Code MCP server for Infrawise Azure FinOps recommendations.
4
+
5
+ ## What it provides
6
+
7
+ Tools exposed to Claude Code:
8
+
9
+ - `get_idle_resources`
10
+ - `get_sku_optimizations`
11
+ - `get_general_recommendations`
12
+ - `get_savings_summary`
13
+
14
+ All tools support optional `subscription_filter` (subscription UUID).
15
+
16
+ ## 5-minute setup
17
+
18
+ ```bash
19
+ # 1) Authenticate once with Azure AD device code flow
20
+ npx @infrawise/mcp-server auth
21
+ ```
22
+
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
+ }
34
+ ```
35
+
36
+ Restart Claude Code.
37
+
38
+ ## Local development
39
+
40
+ ```bash
41
+ cd packages/claude-mcp
42
+ npm install
43
+ npm run build
44
+ node dist/index.js auth
45
+ ```
46
+
47
+ ## Environment variables
48
+
49
+ - `INFRAWISE_API_BASE` (default: `https://api.infrawiseai.com/api`)
50
+ - `INFRAWISE_AZURE_CLIENT_ID` (default: Infrawise API app ID)
51
+ - `INFRAWISE_AZURE_API_SCOPE` (default: `api://<clientId>/delegated_access`)
52
+ - `INFRAWISE_AZURE_AUTHORITY` (default: `https://login.microsoftonline.com/common`)
53
+
54
+ ## Security
55
+
56
+ - Token cache is stored at `~/.infrawise/mcp-credentials.json`
57
+ - Credentials file is written with `0600` mode (best effort on Windows)
58
+ - Server is read-only and uses GET calls only
package/dist/auth.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export declare class NotAuthenticatedError extends Error {
2
+ constructor(message?: string);
3
+ }
4
+ export interface AuthSettings {
5
+ authority: string;
6
+ clientId: string;
7
+ scope: string;
8
+ }
9
+ export declare function runDeviceCodeAuth(): Promise<void>;
10
+ export declare function getAccessToken(): Promise<string>;
package/dist/auth.js ADDED
@@ -0,0 +1,135 @@
1
+ import { PublicClientApplication } from "@azure/msal-node";
2
+ import { promises as fs } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common";
6
+ const DEFAULT_API_CLIENT_ID = "06dc6d06-11f1-4543-bb2c-9c91b263df56";
7
+ const DEFAULT_SCOPE = `api://${DEFAULT_API_CLIENT_ID}/delegated_access`;
8
+ const CREDENTIALS_DIR = ".infrawise";
9
+ const CREDENTIALS_FILE = "mcp-credentials.json";
10
+ export class NotAuthenticatedError extends Error {
11
+ constructor(message = "Not authenticated. Run: npx @infrawise/mcp-server auth") {
12
+ super(message);
13
+ this.name = "NotAuthenticatedError";
14
+ }
15
+ }
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
+ return {
20
+ authority: process.env.INFRAWISE_AZURE_AUTHORITY ?? DEFAULT_AUTHORITY,
21
+ clientId,
22
+ scope,
23
+ };
24
+ }
25
+ function getCredentialFilePath() {
26
+ return path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
27
+ }
28
+ async function readCachePayload() {
29
+ const credentialPath = getCredentialFilePath();
30
+ try {
31
+ const raw = await fs.readFile(credentialPath, "utf8");
32
+ const parsed = JSON.parse(raw);
33
+ if (typeof parsed.cache !== "string" || typeof parsed.clientId !== "string" || typeof parsed.scope !== "string") {
34
+ return null;
35
+ }
36
+ return {
37
+ authority: parsed.authority ?? DEFAULT_AUTHORITY,
38
+ clientId: parsed.clientId,
39
+ scope: parsed.scope,
40
+ homeAccountId: parsed.homeAccountId,
41
+ cache: parsed.cache,
42
+ updatedAt: parsed.updatedAt ?? new Date(0).toISOString(),
43
+ };
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ async function writeCachePayload(payload) {
50
+ const credentialPath = getCredentialFilePath();
51
+ const credentialDir = path.dirname(credentialPath);
52
+ await fs.mkdir(credentialDir, { recursive: true });
53
+ await fs.writeFile(credentialPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
54
+ try {
55
+ await fs.chmod(credentialPath, 0o600);
56
+ }
57
+ catch {
58
+ // Best-effort on Windows.
59
+ }
60
+ }
61
+ async function createClientFromSettings(settings) {
62
+ const payload = await readCachePayload();
63
+ const client = new PublicClientApplication({
64
+ auth: {
65
+ clientId: settings.clientId,
66
+ authority: settings.authority,
67
+ },
68
+ });
69
+ if (payload?.cache) {
70
+ await client.getTokenCache().deserialize(payload.cache);
71
+ }
72
+ return { client, payload };
73
+ }
74
+ async function getPreferredAccount(client, homeAccountId) {
75
+ if (homeAccountId) {
76
+ const exact = await client.getTokenCache().getAccountByHomeId(homeAccountId);
77
+ if (exact) {
78
+ return exact;
79
+ }
80
+ }
81
+ const allAccounts = await client.getTokenCache().getAllAccounts();
82
+ return allAccounts[0] ?? null;
83
+ }
84
+ async function persistClientCache(client, settings, account) {
85
+ const serialized = await client.getTokenCache().serialize();
86
+ await writeCachePayload({
87
+ authority: settings.authority,
88
+ clientId: settings.clientId,
89
+ scope: settings.scope,
90
+ homeAccountId: account?.homeAccountId,
91
+ cache: serialized,
92
+ updatedAt: new Date().toISOString(),
93
+ });
94
+ }
95
+ export async function runDeviceCodeAuth() {
96
+ const settings = getAuthSettings();
97
+ const { client } = await createClientFromSettings(settings);
98
+ const request = {
99
+ scopes: [settings.scope, "openid", "profile"],
100
+ deviceCodeCallback: (response) => {
101
+ const msg = response.message ??
102
+ `To authenticate, visit https://microsoft.com/devicelogin and enter the code: ${response.userCode}`;
103
+ console.error(`[Infrawise MCP] ${msg}`);
104
+ },
105
+ };
106
+ const result = await client.acquireTokenByDeviceCode(request);
107
+ if (!result || !result.account) {
108
+ throw new Error("Device Code authentication did not return an account.");
109
+ }
110
+ await persistClientCache(client, settings, result.account);
111
+ console.error(`[Infrawise MCP] Authentication complete. Token cache saved to ${getCredentialFilePath()}`);
112
+ }
113
+ export async function getAccessToken() {
114
+ const settings = getAuthSettings();
115
+ const { client, payload } = await createClientFromSettings(settings);
116
+ const account = await getPreferredAccount(client, payload?.homeAccountId);
117
+ if (!account) {
118
+ throw new NotAuthenticatedError();
119
+ }
120
+ const request = {
121
+ account,
122
+ scopes: [settings.scope],
123
+ };
124
+ try {
125
+ const result = await client.acquireTokenSilent(request);
126
+ if (!result?.accessToken) {
127
+ throw new NotAuthenticatedError();
128
+ }
129
+ await persistClientCache(client, settings, result.account ?? account);
130
+ return result.accessToken;
131
+ }
132
+ catch {
133
+ throw new NotAuthenticatedError();
134
+ }
135
+ }
@@ -0,0 +1,7 @@
1
+ export type InfrawiseErrorCode = "NOT_AUTHENTICATED" | "NOT_ONBOARDED" | "PENDING_DELEGATION" | "UPSTREAM_ERROR";
2
+ export declare class InfrawiseClientError extends Error {
3
+ code: InfrawiseErrorCode;
4
+ status?: number;
5
+ constructor(code: InfrawiseErrorCode, message: string, status?: number);
6
+ }
7
+ export declare function getInfrawiseData<T>(path: string): Promise<T>;
package/dist/client.js ADDED
@@ -0,0 +1,102 @@
1
+ import { NotAuthenticatedError, getAccessToken } from "./auth.js";
2
+ const DEFAULT_API_BASE = "https://api.infrawiseai.com/api";
3
+ const REQUEST_TIMEOUT_MS = 120_000;
4
+ export class InfrawiseClientError extends Error {
5
+ code;
6
+ status;
7
+ constructor(code, message, status) {
8
+ super(message);
9
+ this.name = "InfrawiseClientError";
10
+ this.code = code;
11
+ this.status = status;
12
+ }
13
+ }
14
+ function getApiBase() {
15
+ const value = process.env.INFRAWISE_API_BASE ?? DEFAULT_API_BASE;
16
+ return value.endsWith("/") ? value.slice(0, -1) : value;
17
+ }
18
+ function getEndpoint(path) {
19
+ const base = getApiBase();
20
+ return `${base}${path.startsWith("/") ? path : `/${path}`}`;
21
+ }
22
+ async function fetchJson(path, token) {
23
+ const controller = new AbortController();
24
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
25
+ try {
26
+ const response = await fetch(getEndpoint(path), {
27
+ method: "GET",
28
+ headers: {
29
+ Authorization: `Bearer ${token}`,
30
+ Accept: "application/json",
31
+ },
32
+ signal: controller.signal,
33
+ });
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);
36
+ }
37
+ if (!response.ok) {
38
+ const body = await response.text();
39
+ throw new InfrawiseClientError("UPSTREAM_ERROR", `Infrawise API error (${response.status}) at ${path}: ${body || response.statusText}`, response.status);
40
+ }
41
+ return {
42
+ data: (await response.json()),
43
+ status: response.status,
44
+ };
45
+ }
46
+ catch (error) {
47
+ if (error instanceof InfrawiseClientError) {
48
+ throw error;
49
+ }
50
+ if (error instanceof NotAuthenticatedError) {
51
+ throw new InfrawiseClientError("NOT_AUTHENTICATED", error.message);
52
+ }
53
+ if (error instanceof Error && error.name === "AbortError") {
54
+ throw new InfrawiseClientError("UPSTREAM_ERROR", "Infrawise API request timed out after 120 seconds.");
55
+ }
56
+ throw new InfrawiseClientError("UPSTREAM_ERROR", `Unexpected Infrawise API error: ${String(error)}`);
57
+ }
58
+ finally {
59
+ clearTimeout(timeout);
60
+ }
61
+ }
62
+ function normalizeOnboardingState(status) {
63
+ const normalized = (status ?? "").toUpperCase();
64
+ if (normalized === "ACTIVE" || normalized === "PENDING_DELEGATION" || normalized === "EXPIRED" || normalized === "SUSPENDED") {
65
+ return normalized;
66
+ }
67
+ return "UNKNOWN";
68
+ }
69
+ async function assertTenantReady(token) {
70
+ const tenantValidation = await fetchJson("/auth/validate-tenant", token);
71
+ if (!tenantValidation.data?.authorized) {
72
+ throw new InfrawiseClientError("NOT_ONBOARDED", "Tenant is not authorized in Infrawise. Complete onboarding at https://infrawiseai.com/onboard", tenantValidation.status);
73
+ }
74
+ const onboarding = await fetchJson("/onboarding/status", token);
75
+ if (!Array.isArray(onboarding.data) || onboarding.data.length === 0) {
76
+ // Legacy allowlisted tenants may have no onboarding records.
77
+ return;
78
+ }
79
+ const states = onboarding.data.map((entry) => normalizeOnboardingState(entry.status));
80
+ if (states.includes("ACTIVE")) {
81
+ return;
82
+ }
83
+ if (states.includes("PENDING_DELEGATION")) {
84
+ throw new InfrawiseClientError("PENDING_DELEGATION", "Infrawise onboarding is pending Lighthouse delegation. Complete setup at https://infrawiseai.com/onboard");
85
+ }
86
+ throw new InfrawiseClientError("NOT_ONBOARDED", "No active Infrawise subscription found. Complete onboarding at https://infrawiseai.com/onboard");
87
+ }
88
+ export async function getInfrawiseData(path) {
89
+ let token;
90
+ try {
91
+ token = await getAccessToken();
92
+ }
93
+ catch (error) {
94
+ if (error instanceof NotAuthenticatedError) {
95
+ throw new InfrawiseClientError("NOT_AUTHENTICATED", error.message);
96
+ }
97
+ throw error;
98
+ }
99
+ await assertTenantReady(token);
100
+ const response = await fetchJson(path, token);
101
+ return response.data;
102
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { runDeviceCodeAuth } from "./auth.js";
6
+ import { InfrawiseClientError } from "./client.js";
7
+ import { getGeneralRecommendations } from "./tools/general-recommendations.js";
8
+ import { getIdleResources } from "./tools/idle-resources.js";
9
+ import { getSavingsSummary } from "./tools/summary.js";
10
+ import { getSkuOptimizations } from "./tools/sku-optimizations.js";
11
+ function toToolSuccess(payload) {
12
+ return {
13
+ content: [
14
+ {
15
+ type: "text",
16
+ text: JSON.stringify(payload, null, 2),
17
+ },
18
+ ],
19
+ };
20
+ }
21
+ function toToolError(error) {
22
+ if (error instanceof InfrawiseClientError) {
23
+ return {
24
+ isError: true,
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: JSON.stringify({
29
+ code: error.code,
30
+ message: error.message,
31
+ status: error.status,
32
+ }, null, 2),
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ return {
38
+ isError: true,
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: JSON.stringify({
43
+ code: "UPSTREAM_ERROR",
44
+ message: error instanceof Error ? error.message : String(error),
45
+ }, null, 2),
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ async function runAuthMode() {
51
+ try {
52
+ await runDeviceCodeAuth();
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ console.error(`[Infrawise MCP] Authentication failed: ${message}`);
57
+ process.exitCode = 1;
58
+ }
59
+ }
60
+ async function runServerMode() {
61
+ const server = new McpServer({
62
+ name: "@infrawise/mcp-server",
63
+ version: "0.1.0",
64
+ });
65
+ const filterSchema = {
66
+ subscription_filter: z
67
+ .string()
68
+ .uuid()
69
+ .optional()
70
+ .describe("Optional subscription ID to filter results. When omitted, returns merged results across all accessible subscriptions."),
71
+ };
72
+ server.tool("get_idle_resources", "Retrieve idle and zombie Azure resources that can be deleted or deallocated for cost savings.", filterSchema, async ({ subscription_filter }) => {
73
+ try {
74
+ const result = await getIdleResources(subscription_filter);
75
+ return toToolSuccess(result);
76
+ }
77
+ catch (error) {
78
+ return toToolError(error);
79
+ }
80
+ });
81
+ server.tool("get_sku_optimizations", "Retrieve rightsizing recommendations for overprovisioned resources.", filterSchema, async ({ subscription_filter }) => {
82
+ try {
83
+ const result = await getSkuOptimizations(subscription_filter);
84
+ return toToolSuccess(result);
85
+ }
86
+ catch (error) {
87
+ return toToolError(error);
88
+ }
89
+ });
90
+ server.tool("get_general_recommendations", "Retrieve strategic purchasing and licensing optimization recommendations.", filterSchema, async ({ subscription_filter }) => {
91
+ try {
92
+ const result = await getGeneralRecommendations(subscription_filter);
93
+ return toToolSuccess(result);
94
+ }
95
+ catch (error) {
96
+ return toToolError(error);
97
+ }
98
+ });
99
+ server.tool("get_savings_summary", "Aggregate savings totals across idle resources, SKU rightsizing, and general recommendations.", filterSchema, async ({ subscription_filter }) => {
100
+ try {
101
+ const result = await getSavingsSummary(subscription_filter);
102
+ return toToolSuccess(result);
103
+ }
104
+ catch (error) {
105
+ return toToolError(error);
106
+ }
107
+ });
108
+ const transport = new StdioServerTransport();
109
+ await server.connect(transport);
110
+ }
111
+ async function main() {
112
+ const mode = process.argv[2];
113
+ if (mode === "auth") {
114
+ await runAuthMode();
115
+ return;
116
+ }
117
+ await runServerMode();
118
+ }
119
+ await main();
@@ -0,0 +1,2 @@
1
+ import { type GeneralRecommendation } from "./types.js";
2
+ export declare function getGeneralRecommendations(subscriptionFilter?: string): Promise<GeneralRecommendation[]>;
@@ -0,0 +1,7 @@
1
+ import { getInfrawiseData } from "../client.js";
2
+ import { filterBySubscription } from "./types.js";
3
+ export async function getGeneralRecommendations(subscriptionFilter) {
4
+ const data = await getInfrawiseData("/general-recommendations");
5
+ const rows = Array.isArray(data) ? data : [];
6
+ return filterBySubscription(rows, subscriptionFilter);
7
+ }
@@ -0,0 +1,2 @@
1
+ import { type IdleResource } from "./types.js";
2
+ export declare function getIdleResources(subscriptionFilter?: string): Promise<IdleResource[]>;
@@ -0,0 +1,7 @@
1
+ import { getInfrawiseData } from "../client.js";
2
+ import { filterBySubscription } from "./types.js";
3
+ export async function getIdleResources(subscriptionFilter) {
4
+ const data = await getInfrawiseData("/idle-resources");
5
+ const rows = Array.isArray(data) ? data : [];
6
+ return filterBySubscription(rows, subscriptionFilter);
7
+ }
@@ -0,0 +1,2 @@
1
+ import { type SkuOptimization } from "./types.js";
2
+ export declare function getSkuOptimizations(subscriptionFilter?: string): Promise<SkuOptimization[]>;
@@ -0,0 +1,7 @@
1
+ import { getInfrawiseData } from "../client.js";
2
+ import { filterBySubscription } from "./types.js";
3
+ export async function getSkuOptimizations(subscriptionFilter) {
4
+ const data = await getInfrawiseData("/sku-optimizations");
5
+ const rows = Array.isArray(data) ? data : [];
6
+ return filterBySubscription(rows, subscriptionFilter);
7
+ }
@@ -0,0 +1,23 @@
1
+ interface SavingsBucket {
2
+ count: number;
3
+ totalMonthlySavings: number;
4
+ totalAnnualSavings: number;
5
+ }
6
+ interface IdleSavingsBucket extends SavingsBucket {
7
+ highRisk: number;
8
+ mediumRisk: number;
9
+ lowRisk: number;
10
+ }
11
+ export interface SavingsSummary {
12
+ idleResources: IdleSavingsBucket;
13
+ skuOptimizations: SavingsBucket;
14
+ generalRecommendations: SavingsBucket;
15
+ grandTotal: {
16
+ totalMonthlySavings: number;
17
+ totalAnnualSavings: number;
18
+ };
19
+ subscription: string;
20
+ generatedAt: string;
21
+ }
22
+ export declare function getSavingsSummary(subscriptionFilter?: string): Promise<SavingsSummary>;
23
+ export {};
@@ -0,0 +1,79 @@
1
+ import { getGeneralRecommendations } from "./general-recommendations.js";
2
+ import { getIdleResources } from "./idle-resources.js";
3
+ import { getSkuOptimizations } from "./sku-optimizations.js";
4
+ import { toNumber } from "./types.js";
5
+ function roundCurrency(value) {
6
+ return Math.round(value * 100) / 100;
7
+ }
8
+ export async function getSavingsSummary(subscriptionFilter) {
9
+ const [idleResources, skuOptimizations, generalRecommendations] = await Promise.all([
10
+ getIdleResources(subscriptionFilter),
11
+ getSkuOptimizations(subscriptionFilter),
12
+ getGeneralRecommendations(subscriptionFilter),
13
+ ]);
14
+ const idleMonthly = idleResources.reduce((sum, item) => sum + toNumber(item.monthlyCost), 0);
15
+ const idleAnnual = idleResources.reduce((sum, item) => {
16
+ const annual = toNumber(item.annualCost);
17
+ if (annual > 0) {
18
+ return sum + annual;
19
+ }
20
+ return sum + toNumber(item.monthlyCost) * 12;
21
+ }, 0);
22
+ const skuMonthly = skuOptimizations.reduce((sum, item) => sum + toNumber(item.monthlySavings), 0);
23
+ const skuAnnual = skuOptimizations.reduce((sum, item) => {
24
+ const annual = toNumber(item.annualSavings);
25
+ if (annual > 0) {
26
+ return sum + annual;
27
+ }
28
+ return sum + toNumber(item.monthlySavings) * 12;
29
+ }, 0);
30
+ const generalMonthly = generalRecommendations.reduce((sum, item) => sum + toNumber(item.estimatedMonthlySavings), 0);
31
+ const generalAnnual = generalRecommendations.reduce((sum, item) => {
32
+ const annual = toNumber(item.estimatedAnnualSavings);
33
+ if (annual > 0) {
34
+ return sum + annual;
35
+ }
36
+ return sum + toNumber(item.estimatedMonthlySavings) * 12;
37
+ }, 0);
38
+ const idleRiskCounts = idleResources.reduce((acc, item) => {
39
+ const risk = (item.riskLevel ?? "").toLowerCase();
40
+ if (risk === "high") {
41
+ acc.highRisk += 1;
42
+ }
43
+ else if (risk === "medium") {
44
+ acc.mediumRisk += 1;
45
+ }
46
+ else if (risk === "low") {
47
+ acc.lowRisk += 1;
48
+ }
49
+ return acc;
50
+ }, { highRisk: 0, mediumRisk: 0, lowRisk: 0 });
51
+ const grandMonthly = idleMonthly + skuMonthly + generalMonthly;
52
+ const grandAnnual = idleAnnual + skuAnnual + generalAnnual;
53
+ return {
54
+ idleResources: {
55
+ count: idleResources.length,
56
+ totalMonthlySavings: roundCurrency(idleMonthly),
57
+ totalAnnualSavings: roundCurrency(idleAnnual),
58
+ highRisk: idleRiskCounts.highRisk,
59
+ mediumRisk: idleRiskCounts.mediumRisk,
60
+ lowRisk: idleRiskCounts.lowRisk,
61
+ },
62
+ skuOptimizations: {
63
+ count: skuOptimizations.length,
64
+ totalMonthlySavings: roundCurrency(skuMonthly),
65
+ totalAnnualSavings: roundCurrency(skuAnnual),
66
+ },
67
+ generalRecommendations: {
68
+ count: generalRecommendations.length,
69
+ totalMonthlySavings: roundCurrency(generalMonthly),
70
+ totalAnnualSavings: roundCurrency(generalAnnual),
71
+ },
72
+ grandTotal: {
73
+ totalMonthlySavings: roundCurrency(grandMonthly),
74
+ totalAnnualSavings: roundCurrency(grandAnnual),
75
+ },
76
+ subscription: subscriptionFilter ?? "all",
77
+ generatedAt: new Date().toISOString(),
78
+ };
79
+ }
@@ -0,0 +1,48 @@
1
+ export interface IdleResource {
2
+ resourceId?: string;
3
+ resourceName?: string;
4
+ resourceType?: string;
5
+ monthlyCost?: number | null;
6
+ annualCost?: number | null;
7
+ lastActivityDays?: number | null;
8
+ utilizationPercent?: number | null;
9
+ recommendation?: string;
10
+ reason?: string;
11
+ riskLevel?: string;
12
+ difficulty?: string;
13
+ }
14
+ export interface SkuOptimization {
15
+ resourceId?: string;
16
+ resourceName?: string;
17
+ resourceType?: string;
18
+ currentSku?: string;
19
+ recommendedSku?: string;
20
+ currentMonthlyCost?: number | null;
21
+ projectedMonthlyCost?: number | null;
22
+ monthlySavings?: number | null;
23
+ annualSavings?: number | null;
24
+ reason?: string;
25
+ riskLevel?: string;
26
+ difficulty?: string;
27
+ }
28
+ export interface GeneralRecommendation {
29
+ resourceId?: string;
30
+ resourceName?: string;
31
+ resourceType?: string;
32
+ category?: string;
33
+ title?: string;
34
+ reason?: string;
35
+ estimatedMonthlySavings?: number | null;
36
+ estimatedAnnualSavings?: number | null;
37
+ impact?: string;
38
+ riskLevel?: string;
39
+ difficulty?: string;
40
+ }
41
+ export interface ToolFilterInput {
42
+ subscription_filter?: string;
43
+ }
44
+ export declare function extractSubscriptionId(resourceId: string | undefined): string | null;
45
+ export declare function filterBySubscription<T extends {
46
+ resourceId?: string;
47
+ }>(items: T[], subscriptionFilter?: string): T[];
48
+ export declare function toNumber(value: number | null | undefined): number;
@@ -0,0 +1,17 @@
1
+ export function extractSubscriptionId(resourceId) {
2
+ if (!resourceId) {
3
+ return null;
4
+ }
5
+ const match = resourceId.match(/\/subscriptions\/([^/]+)/i);
6
+ return match?.[1]?.toLowerCase() ?? null;
7
+ }
8
+ export function filterBySubscription(items, subscriptionFilter) {
9
+ if (!subscriptionFilter) {
10
+ return items;
11
+ }
12
+ const normalized = subscriptionFilter.trim().toLowerCase();
13
+ return items.filter((item) => extractSubscriptionId(item.resourceId) === normalized);
14
+ }
15
+ export function toNumber(value) {
16
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@infrawise/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Infrawise MCP server for Claude Code",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "infrawise-mcp": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist/",
13
+ "README.md"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/admonitor-inc/infrawise.git",
18
+ "directory": "packages/claude-mcp"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "clean": "rimraf dist",
23
+ "prepublishOnly": "npm run clean && npm run build"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "claude",
31
+ "azure",
32
+ "finops",
33
+ "infrawise"
34
+ ],
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@azure/msal-node": "^3.8.3",
38
+ "@modelcontextprotocol/sdk": "^1.18.1",
39
+ "zod": "^3.24.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.10.6",
43
+ "rimraf": "^6.0.1",
44
+ "typescript": "^5.7.2"
45
+ }
46
+ }