@finctl/mcp 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 +224 -0
- package/dist/auth/authenticator.d.ts +37 -0
- package/dist/auth/authenticator.js +66 -0
- package/dist/auth/authenticator.js.map +1 -0
- package/dist/auth/backend-store.d.ts +19 -0
- package/dist/auth/backend-store.js +57 -0
- package/dist/auth/backend-store.js.map +1 -0
- package/dist/auth/context.d.ts +23 -0
- package/dist/auth/context.js +30 -0
- package/dist/auth/context.js.map +1 -0
- package/dist/auth/rate-limit.d.ts +17 -0
- package/dist/auth/rate-limit.js +22 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth/store.d.ts +47 -0
- package/dist/auth/store.js +62 -0
- package/dist/auth/store.js.map +1 -0
- package/dist/client/finctl-client.d.ts +212 -0
- package/dist/client/finctl-client.js +131 -0
- package/dist/client/finctl-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +35 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/catalog.d.ts +36 -0
- package/dist/tools/catalog.js +417 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/transports/http.d.ts +17 -0
- package/dist/transports/http.js +99 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/stdio.d.ts +14 -0
- package/dist/transports/stdio.js +37 -0
- package/dist/transports/stdio.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +22 -0
- package/dist/version.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed HTTP client for the FinCtl backend (the `dashboard-api` Lambda in the
|
|
3
|
+
* finctl-core repo). The MCP server is a client of that API: cost tools proxy
|
|
4
|
+
* to existing analyzer endpoints rather than querying DynamoDB directly.
|
|
5
|
+
*
|
|
6
|
+
* Auth: the backend authenticates with a Cognito Bearer token and scopes data
|
|
7
|
+
* by customer. Until the API-key -> backend-token exchange lands (backend work,
|
|
8
|
+
* tracked alongside distribution in FIN-1470), the MCP server forwards:
|
|
9
|
+
* - Authorization: Bearer <FINCTL_BACKEND_TOKEN> (service/customer JWT)
|
|
10
|
+
* - X-Customer-Id: <customerId> (from the authenticated key)
|
|
11
|
+
* The backend resolves and enforces the customer scope from these.
|
|
12
|
+
*/
|
|
13
|
+
/** A FinCtl backend error surfaced to the caller without a stack trace. */
|
|
14
|
+
export declare class FinCtlApiError extends Error {
|
|
15
|
+
readonly status: number;
|
|
16
|
+
constructor(status: number, message: string);
|
|
17
|
+
}
|
|
18
|
+
export interface AccountSpendRow {
|
|
19
|
+
accountId: string;
|
|
20
|
+
accountName: string;
|
|
21
|
+
mtdSpend: number;
|
|
22
|
+
status: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AccountSpendSummary {
|
|
25
|
+
periodStart: string;
|
|
26
|
+
periodEnd: string;
|
|
27
|
+
total: number;
|
|
28
|
+
currency: string;
|
|
29
|
+
accounts: AccountSpendRow[];
|
|
30
|
+
asOf: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TopCostResource {
|
|
33
|
+
resourceId: string;
|
|
34
|
+
name: string;
|
|
35
|
+
service: string;
|
|
36
|
+
region: string;
|
|
37
|
+
accountId: string;
|
|
38
|
+
cost: number;
|
|
39
|
+
costSource: "actual" | "savings";
|
|
40
|
+
}
|
|
41
|
+
export interface TopCostResponse {
|
|
42
|
+
resources: TopCostResource[];
|
|
43
|
+
total: number;
|
|
44
|
+
}
|
|
45
|
+
export interface Recommendation {
|
|
46
|
+
resourceId: string;
|
|
47
|
+
resourceType: string;
|
|
48
|
+
accountId?: string;
|
|
49
|
+
estimatedMonthlySavings: number;
|
|
50
|
+
confidence: "HIGH" | "MEDIUM" | "LOW";
|
|
51
|
+
actionType: string;
|
|
52
|
+
reasoning?: string;
|
|
53
|
+
currentConfiguration?: {
|
|
54
|
+
instanceType?: string;
|
|
55
|
+
summary?: string;
|
|
56
|
+
} | {
|
|
57
|
+
value?: string;
|
|
58
|
+
};
|
|
59
|
+
recommendedConfiguration?: {
|
|
60
|
+
instanceType?: string;
|
|
61
|
+
summary?: string;
|
|
62
|
+
} | {
|
|
63
|
+
value?: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export interface RecommendationsResponse {
|
|
67
|
+
recommendations: Recommendation[];
|
|
68
|
+
}
|
|
69
|
+
export interface Account {
|
|
70
|
+
accountId: string;
|
|
71
|
+
accountName?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface AccountsResponse {
|
|
74
|
+
accounts: Account[];
|
|
75
|
+
}
|
|
76
|
+
export interface SpUtilization {
|
|
77
|
+
orgWideUtilizationPct: number;
|
|
78
|
+
totalSPCommitment: number;
|
|
79
|
+
unusedCommitment: number;
|
|
80
|
+
}
|
|
81
|
+
export interface RiUtilization {
|
|
82
|
+
orgWideUtilizationPct: number;
|
|
83
|
+
totalRICost: number;
|
|
84
|
+
unusedRICost: number;
|
|
85
|
+
}
|
|
86
|
+
export interface ForecastSummary {
|
|
87
|
+
totalCurrentCost: number;
|
|
88
|
+
totalForecastCost: number;
|
|
89
|
+
projectedGrowth: {
|
|
90
|
+
absolute: number;
|
|
91
|
+
percentage: number;
|
|
92
|
+
};
|
|
93
|
+
confidence: number;
|
|
94
|
+
period: {
|
|
95
|
+
start: string;
|
|
96
|
+
end: string;
|
|
97
|
+
};
|
|
98
|
+
budgetStatus?: {
|
|
99
|
+
budgetAmount?: number;
|
|
100
|
+
projectedSpend: number;
|
|
101
|
+
variance: number;
|
|
102
|
+
warningTriggered: boolean;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export interface ForecastResponse {
|
|
106
|
+
forecast: ForecastSummary | null;
|
|
107
|
+
isEmpty: boolean;
|
|
108
|
+
lastUpdated?: string;
|
|
109
|
+
message?: string;
|
|
110
|
+
}
|
|
111
|
+
export interface Anomaly {
|
|
112
|
+
anomalyType: string;
|
|
113
|
+
severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
|
|
114
|
+
service: string;
|
|
115
|
+
accountId?: string;
|
|
116
|
+
detectedAt: string;
|
|
117
|
+
currentValue: number;
|
|
118
|
+
expectedValue: number;
|
|
119
|
+
deviation?: number;
|
|
120
|
+
}
|
|
121
|
+
export interface AnomaliesResponse {
|
|
122
|
+
anomalies: Anomaly[];
|
|
123
|
+
total: number;
|
|
124
|
+
}
|
|
125
|
+
export interface Budget {
|
|
126
|
+
id: string;
|
|
127
|
+
name: string;
|
|
128
|
+
amount: number;
|
|
129
|
+
currentSpend: number;
|
|
130
|
+
forecastedSpend: number;
|
|
131
|
+
status: "ACTIVE" | "EXCEEDED" | "WARNING" | "INACTIVE";
|
|
132
|
+
scope?: {
|
|
133
|
+
type: string;
|
|
134
|
+
value?: string;
|
|
135
|
+
};
|
|
136
|
+
filters?: {
|
|
137
|
+
accounts?: string[];
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export interface BudgetsResponse {
|
|
141
|
+
budgets: Budget[];
|
|
142
|
+
total: number;
|
|
143
|
+
}
|
|
144
|
+
/** Surface the FinCtl cost API to the tool layer. */
|
|
145
|
+
export interface CostClient {
|
|
146
|
+
spendSummary(customerId: string): Promise<AccountSpendSummary>;
|
|
147
|
+
topCost(customerId: string, limit: number): Promise<TopCostResponse>;
|
|
148
|
+
recommendations(customerId: string, opts: {
|
|
149
|
+
limit: number;
|
|
150
|
+
resourceType?: string;
|
|
151
|
+
accountId?: string;
|
|
152
|
+
}): Promise<RecommendationsResponse>;
|
|
153
|
+
accounts(customerId: string): Promise<AccountsResponse>;
|
|
154
|
+
spUtilization(customerId: string): Promise<SpUtilization>;
|
|
155
|
+
riUtilization(customerId: string): Promise<RiUtilization>;
|
|
156
|
+
forecast(customerId: string): Promise<ForecastResponse>;
|
|
157
|
+
anomalies(customerId: string, opts: {
|
|
158
|
+
limit: number;
|
|
159
|
+
timeRange: string;
|
|
160
|
+
}): Promise<AnomaliesResponse>;
|
|
161
|
+
budgets(customerId: string): Promise<BudgetsResponse>;
|
|
162
|
+
}
|
|
163
|
+
export interface FinCtlClientOptions {
|
|
164
|
+
baseUrl: string;
|
|
165
|
+
/**
|
|
166
|
+
* Shared MCP service secret (sent as X-Finctl-Mcp-Secret; = backend
|
|
167
|
+
* MCP_VALIDATE_SECRET). With X-Customer-Id this is how the backend
|
|
168
|
+
* authenticates and scopes MCP requests (FIN-2648).
|
|
169
|
+
*/
|
|
170
|
+
serviceSecret?: string;
|
|
171
|
+
/** Optional Bearer token (Cognito JWT), if a deployment uses one. */
|
|
172
|
+
token?: string;
|
|
173
|
+
/** Per-request timeout in ms. */
|
|
174
|
+
timeoutMs?: number;
|
|
175
|
+
fetchImpl?: typeof fetch;
|
|
176
|
+
}
|
|
177
|
+
export declare class FinCtlClient implements CostClient {
|
|
178
|
+
private readonly baseUrl;
|
|
179
|
+
private readonly serviceSecret?;
|
|
180
|
+
private readonly token?;
|
|
181
|
+
private readonly timeoutMs;
|
|
182
|
+
private readonly fetchImpl;
|
|
183
|
+
constructor(opts: FinCtlClientOptions);
|
|
184
|
+
private get;
|
|
185
|
+
spendSummary(customerId: string): Promise<AccountSpendSummary>;
|
|
186
|
+
topCost(customerId: string, limit: number): Promise<TopCostResponse>;
|
|
187
|
+
recommendations(customerId: string, opts: {
|
|
188
|
+
limit: number;
|
|
189
|
+
resourceType?: string;
|
|
190
|
+
accountId?: string;
|
|
191
|
+
}): Promise<RecommendationsResponse>;
|
|
192
|
+
accounts(customerId: string): Promise<AccountsResponse>;
|
|
193
|
+
forecast(customerId: string): Promise<ForecastResponse>;
|
|
194
|
+
anomalies(customerId: string, opts: {
|
|
195
|
+
limit: number;
|
|
196
|
+
timeRange: string;
|
|
197
|
+
}): Promise<AnomaliesResponse>;
|
|
198
|
+
budgets(customerId: string): Promise<BudgetsResponse>;
|
|
199
|
+
spUtilization(customerId: string): Promise<SpUtilization>;
|
|
200
|
+
riUtilization(customerId: string): Promise<RiUtilization>;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Build a client from environment configuration.
|
|
204
|
+
*
|
|
205
|
+
* FINCTL_ENDPOINT | FINCTL_DASHBOARD_API_URL backend base URL (required for live data)
|
|
206
|
+
* FINCTL_MCP_SERVICE_SECRET shared service secret (= backend MCP_VALIDATE_SECRET)
|
|
207
|
+
* FINCTL_BACKEND_TOKEN optional Bearer token
|
|
208
|
+
*
|
|
209
|
+
* Returns null when no endpoint is configured, so the server can run (handshake,
|
|
210
|
+
* tool list) without a backend and tools report a clear "not configured" error.
|
|
211
|
+
*/
|
|
212
|
+
export declare function createClientFromEnv(env?: NodeJS.ProcessEnv): FinCtlClient | null;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed HTTP client for the FinCtl backend (the `dashboard-api` Lambda in the
|
|
3
|
+
* finctl-core repo). The MCP server is a client of that API: cost tools proxy
|
|
4
|
+
* to existing analyzer endpoints rather than querying DynamoDB directly.
|
|
5
|
+
*
|
|
6
|
+
* Auth: the backend authenticates with a Cognito Bearer token and scopes data
|
|
7
|
+
* by customer. Until the API-key -> backend-token exchange lands (backend work,
|
|
8
|
+
* tracked alongside distribution in FIN-1470), the MCP server forwards:
|
|
9
|
+
* - Authorization: Bearer <FINCTL_BACKEND_TOKEN> (service/customer JWT)
|
|
10
|
+
* - X-Customer-Id: <customerId> (from the authenticated key)
|
|
11
|
+
* The backend resolves and enforces the customer scope from these.
|
|
12
|
+
*/
|
|
13
|
+
/** A FinCtl backend error surfaced to the caller without a stack trace. */
|
|
14
|
+
export class FinCtlApiError extends Error {
|
|
15
|
+
status;
|
|
16
|
+
constructor(status, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.name = "FinCtlApiError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class FinCtlClient {
|
|
23
|
+
baseUrl;
|
|
24
|
+
serviceSecret;
|
|
25
|
+
token;
|
|
26
|
+
timeoutMs;
|
|
27
|
+
fetchImpl;
|
|
28
|
+
constructor(opts) {
|
|
29
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
30
|
+
this.serviceSecret = opts.serviceSecret;
|
|
31
|
+
this.token = opts.token;
|
|
32
|
+
this.timeoutMs = opts.timeoutMs ?? 15_000;
|
|
33
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
34
|
+
}
|
|
35
|
+
async get(path, customerId, query) {
|
|
36
|
+
const url = new URL(this.baseUrl + path);
|
|
37
|
+
for (const [k, v] of Object.entries(query ?? {})) {
|
|
38
|
+
if (v !== undefined)
|
|
39
|
+
url.searchParams.set(k, String(v));
|
|
40
|
+
}
|
|
41
|
+
const headers = {
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
"X-Customer-Id": customerId,
|
|
44
|
+
};
|
|
45
|
+
if (this.serviceSecret)
|
|
46
|
+
headers["X-Finctl-Mcp-Secret"] = this.serviceSecret;
|
|
47
|
+
if (this.token)
|
|
48
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await this.fetchImpl(url, { headers, signal: controller.signal });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const reason = err.name === "AbortError" ? "request timed out" : err.message;
|
|
57
|
+
throw new FinCtlApiError(0, `Could not reach FinCtl API: ${reason}`);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
// Surface the backend's message if present, never a stack trace.
|
|
64
|
+
let detail = res.statusText;
|
|
65
|
+
try {
|
|
66
|
+
const body = (await res.json());
|
|
67
|
+
detail = body.message ?? body.error ?? detail;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* non-JSON error body */
|
|
71
|
+
}
|
|
72
|
+
throw new FinCtlApiError(res.status, `FinCtl API ${path} failed (${res.status}): ${detail}`);
|
|
73
|
+
}
|
|
74
|
+
return (await res.json());
|
|
75
|
+
}
|
|
76
|
+
spendSummary(customerId) {
|
|
77
|
+
return this.get("/api/accounts/spend-summary", customerId);
|
|
78
|
+
}
|
|
79
|
+
topCost(customerId, limit) {
|
|
80
|
+
return this.get("/api/resources/top-cost", customerId, { limit });
|
|
81
|
+
}
|
|
82
|
+
recommendations(customerId, opts) {
|
|
83
|
+
return this.get("/api/recommendations", customerId, {
|
|
84
|
+
limit: opts.limit,
|
|
85
|
+
resourceType: opts.resourceType,
|
|
86
|
+
accountId: opts.accountId,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
accounts(customerId) {
|
|
90
|
+
return this.get("/api/accounts", customerId);
|
|
91
|
+
}
|
|
92
|
+
forecast(customerId) {
|
|
93
|
+
return this.get("/api/forecast", customerId);
|
|
94
|
+
}
|
|
95
|
+
anomalies(customerId, opts) {
|
|
96
|
+
return this.get("/api/anomalies", customerId, {
|
|
97
|
+
limit: opts.limit,
|
|
98
|
+
timeRange: opts.timeRange,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
budgets(customerId) {
|
|
102
|
+
return this.get("/api/budgets", customerId);
|
|
103
|
+
}
|
|
104
|
+
spUtilization(customerId) {
|
|
105
|
+
return this.get("/api/org/sp-utilization", customerId);
|
|
106
|
+
}
|
|
107
|
+
riUtilization(customerId) {
|
|
108
|
+
return this.get("/api/org/ri-utilization", customerId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build a client from environment configuration.
|
|
113
|
+
*
|
|
114
|
+
* FINCTL_ENDPOINT | FINCTL_DASHBOARD_API_URL backend base URL (required for live data)
|
|
115
|
+
* FINCTL_MCP_SERVICE_SECRET shared service secret (= backend MCP_VALIDATE_SECRET)
|
|
116
|
+
* FINCTL_BACKEND_TOKEN optional Bearer token
|
|
117
|
+
*
|
|
118
|
+
* Returns null when no endpoint is configured, so the server can run (handshake,
|
|
119
|
+
* tool list) without a backend and tools report a clear "not configured" error.
|
|
120
|
+
*/
|
|
121
|
+
export function createClientFromEnv(env = process.env) {
|
|
122
|
+
const baseUrl = (env.FINCTL_ENDPOINT || env.FINCTL_DASHBOARD_API_URL)?.trim();
|
|
123
|
+
if (!baseUrl)
|
|
124
|
+
return null;
|
|
125
|
+
return new FinCtlClient({
|
|
126
|
+
baseUrl,
|
|
127
|
+
serviceSecret: env.FINCTL_MCP_SERVICE_SECRET?.trim() || undefined,
|
|
128
|
+
token: env.FINCTL_BACKEND_TOKEN?.trim() || undefined,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=finctl-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finctl-client.js","sourceRoot":"","sources":["../../src/client/finctl-client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runStdio } from "./transports/stdio.js";
|
|
3
|
+
import { runHttp } from "./transports/http.js";
|
|
4
|
+
import { getVersion } from "./version.js";
|
|
5
|
+
/**
|
|
6
|
+
* FinCtl MCP server entrypoint.
|
|
7
|
+
*
|
|
8
|
+
* Default transport is stdio (for local IDE integrations). Pass --http to run
|
|
9
|
+
* the hosted Streamable HTTP/SSE transport instead.
|
|
10
|
+
*
|
|
11
|
+
* finctl-mcp # stdio
|
|
12
|
+
* finctl-mcp --http [--port 8080] # HTTP/SSE (default port 3000)
|
|
13
|
+
* finctl-mcp --endpoint <url> # point at a FinCtl backend (else FINCTL_ENDPOINT)
|
|
14
|
+
* finctl-mcp --version # print version and exit
|
|
15
|
+
* finctl-mcp --help # print usage and exit
|
|
16
|
+
*/
|
|
17
|
+
async function main() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
// --version / --help short-circuit before any transport or auth, so they work
|
|
20
|
+
// without FINCTL_API_KEY (e.g. `npx @finctl/mcp --version`).
|
|
21
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
22
|
+
process.stdout.write(`${getVersion()}\n`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
26
|
+
printHelp();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// --endpoint overrides the backend URL the cost client reads from the env.
|
|
30
|
+
const endpoint = parseFlag(args, "--endpoint");
|
|
31
|
+
if (endpoint)
|
|
32
|
+
process.env.FINCTL_ENDPOINT = endpoint;
|
|
33
|
+
if (args.includes("--http")) {
|
|
34
|
+
const port = parsePort(args) ?? (Number(process.env.PORT) || 3000);
|
|
35
|
+
await runHttp(port);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await runStdio();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Read the value following a `--flag` in argv, if present. */
|
|
42
|
+
function parseFlag(args, flag) {
|
|
43
|
+
const i = args.indexOf(flag);
|
|
44
|
+
if (i === -1 || i + 1 >= args.length)
|
|
45
|
+
return undefined;
|
|
46
|
+
return args[i + 1];
|
|
47
|
+
}
|
|
48
|
+
function parsePort(args) {
|
|
49
|
+
const raw = parseFlag(args, "--port");
|
|
50
|
+
if (raw === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const port = Number(raw);
|
|
53
|
+
return Number.isInteger(port) && port > 0 ? port : undefined;
|
|
54
|
+
}
|
|
55
|
+
function printHelp() {
|
|
56
|
+
process.stdout.write(`finctl-mcp ${getVersion()} — FinCtl MCP server
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
finctl-mcp [options]
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--http Run the HTTP/SSE transport instead of stdio
|
|
63
|
+
--port <n> HTTP port (default 3000; or PORT env)
|
|
64
|
+
--endpoint <url> FinCtl backend base URL (or FINCTL_ENDPOINT env)
|
|
65
|
+
-v, --version Print version and exit
|
|
66
|
+
-h, --help Print this help and exit
|
|
67
|
+
|
|
68
|
+
Environment:
|
|
69
|
+
FINCTL_API_KEY Customer API key (required to start the server)
|
|
70
|
+
FINCTL_ENDPOINT FinCtl backend base URL
|
|
71
|
+
FINCTL_BACKEND_TOKEN Bearer token the backend accepts
|
|
72
|
+
|
|
73
|
+
Docs: https://github.com/finctl/finctl-mcp
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
main().catch((err) => {
|
|
77
|
+
console.error("[finctl-mcp] fatal:", err);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEnC,8EAA8E;IAC9E,6DAA6D;IAC7D,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,CAAC;QAC1C,OAAO;IACT,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,SAAS,EAAE,CAAC;QACZ,OAAO;IACT,CAAC;IAED,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC/C,IAAI,QAAQ;QAAE,OAAO,CAAC,GAAG,CAAC,eAAe,GAAG,QAAQ,CAAC;IAErD,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;QACnE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;SAAM,CAAC;QACN,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,+DAA+D;AAC/D,SAAS,SAAS,CAAC,IAAc,EAAE,IAAY;IAC7C,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACvD,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/D,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,cAAc,UAAU,EAAE;;;;;;;;;;;;;;;;;;CAkB7B,CACE,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/** Package identity reported during MCP capability negotiation. */
|
|
3
|
+
export declare const SERVER_INFO: {
|
|
4
|
+
readonly name: "finctl-mcp";
|
|
5
|
+
readonly version: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Build a fully-configured FinCtl MCP server with the entire tool catalog
|
|
9
|
+
* registered. Transport-agnostic — connect the returned server to a stdio or
|
|
10
|
+
* HTTP/SSE transport (see src/transports/).
|
|
11
|
+
*/
|
|
12
|
+
export declare function createServer(): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { buildTools } from "./tools/catalog.js";
|
|
3
|
+
import { createClientFromEnv } from "./client/finctl-client.js";
|
|
4
|
+
import { getVersion } from "./version.js";
|
|
5
|
+
/** Package identity reported during MCP capability negotiation. */
|
|
6
|
+
export const SERVER_INFO = {
|
|
7
|
+
name: "finctl-mcp",
|
|
8
|
+
version: getVersion(),
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Build a fully-configured FinCtl MCP server with the entire tool catalog
|
|
12
|
+
* registered. Transport-agnostic — connect the returned server to a stdio or
|
|
13
|
+
* HTTP/SSE transport (see src/transports/).
|
|
14
|
+
*/
|
|
15
|
+
export function createServer() {
|
|
16
|
+
const server = new McpServer(SERVER_INFO, {
|
|
17
|
+
capabilities: { tools: {} },
|
|
18
|
+
instructions: "FinCtl exposes AWS cost intelligence. Use these tools to answer questions about " +
|
|
19
|
+
"spend, top services, savings recommendations, rightsizing, forecasts, anomalies, " +
|
|
20
|
+
"and linked accounts. All data is scoped to the authenticated customer's accounts.",
|
|
21
|
+
});
|
|
22
|
+
const tools = buildTools({ cost: createClientFromEnv() });
|
|
23
|
+
for (const tool of tools) {
|
|
24
|
+
server.registerTool(tool.name, {
|
|
25
|
+
title: tool.title,
|
|
26
|
+
description: tool.description,
|
|
27
|
+
inputSchema: tool.inputSchema,
|
|
28
|
+
},
|
|
29
|
+
// The catalog handler validates/defaults via the same Zod shape the SDK
|
|
30
|
+
// uses for the wire schema, so the parsed args line up.
|
|
31
|
+
tool.handler);
|
|
32
|
+
}
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,mEAAmE;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,UAAU,EAAE;CACb,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,WAAW,EAAE;QACxC,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAC3B,YAAY,EACV,kFAAkF;YAClF,mFAAmF;YACnF,mFAAmF;KACtF,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,UAAU,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,CAAC,CAAC;IAC1D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,IAAI,EACT;YACE,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B;QACD,wEAAwE;QACxE,wDAAwD;QACxD,IAAI,CAAC,OAAoD,CAC1D,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z, type ZodRawShape } from "zod";
|
|
2
|
+
import { type CostClient } from "../client/finctl-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* The FinCtl MCP tool catalog.
|
|
5
|
+
*
|
|
6
|
+
* Each entry defines the tool name, a human-readable title, the description the
|
|
7
|
+
* customer's AI assistant reads to decide when to call it, and the input schema
|
|
8
|
+
* (a Zod raw shape — keys map to named tool arguments).
|
|
9
|
+
*
|
|
10
|
+
* The four core cost-query tools (get_cost_summary, list_top_services,
|
|
11
|
+
* get_recommendations, get_savings_plans_coverage) proxy to the FinCtl backend
|
|
12
|
+
* via {@link CostClient} (FIN-1467). The remaining tools are still stubs; their
|
|
13
|
+
* data wiring lands in FIN-1468 (account/rightsizing) and FIN-1469
|
|
14
|
+
* (forecast/anomalies).
|
|
15
|
+
*/
|
|
16
|
+
/** Shape of a single MCP tool result. */
|
|
17
|
+
export interface ToolResult {
|
|
18
|
+
content: {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Dependencies injected into tool handlers. */
|
|
25
|
+
export interface ToolDeps {
|
|
26
|
+
/** FinCtl backend client, or null when no endpoint is configured. */
|
|
27
|
+
cost: CostClient | null;
|
|
28
|
+
}
|
|
29
|
+
export interface ToolDefinition<S extends ZodRawShape = ZodRawShape> {
|
|
30
|
+
name: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
inputSchema: S;
|
|
34
|
+
handler: (args: z.objectOutputType<S, z.ZodTypeAny>) => Promise<ToolResult>;
|
|
35
|
+
}
|
|
36
|
+
export declare function buildTools(deps: ToolDeps): ToolDefinition[];
|