finops-mcp 0.3.0__tar.gz

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.
Files changed (112) hide show
  1. finops_mcp-0.3.0/.env.example +74 -0
  2. finops_mcp-0.3.0/.gitignore +51 -0
  3. finops_mcp-0.3.0/PKG-INFO +30 -0
  4. finops_mcp-0.3.0/claude_mcp_config.json +12 -0
  5. finops_mcp-0.3.0/finopsmcp/.env.example +74 -0
  6. finops_mcp-0.3.0/finopsmcp/.gitignore +51 -0
  7. finops_mcp-0.3.0/finopsmcp/claude_mcp_config.json +12 -0
  8. finops_mcp-0.3.0/finopsmcp/pyproject.toml +45 -0
  9. finops_mcp-0.3.0/finopsmcp/src/finops/__init__.py +0 -0
  10. finops_mcp-0.3.0/finopsmcp/src/finops/anomaly/__init__.py +0 -0
  11. finops_mcp-0.3.0/finopsmcp/src/finops/anomaly/detector.py +170 -0
  12. finops_mcp-0.3.0/finopsmcp/src/finops/anomaly/seasonality.py +131 -0
  13. finops_mcp-0.3.0/finopsmcp/src/finops/attribution/__init__.py +0 -0
  14. finops_mcp-0.3.0/finopsmcp/src/finops/attribution/fetcher.py +211 -0
  15. finops_mcp-0.3.0/finopsmcp/src/finops/attribution/mapper.py +160 -0
  16. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/__init__.py +0 -0
  17. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/aws.py +176 -0
  18. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/azure.py +142 -0
  19. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/base.py +54 -0
  20. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/gcp.py +165 -0
  21. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/invoice/__init__.py +0 -0
  22. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/invoice/parser.py +324 -0
  23. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/__init__.py +0 -0
  24. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/cloudflare.py +102 -0
  25. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/datadog.py +97 -0
  26. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/github.py +133 -0
  27. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/mongodb_atlas.py +105 -0
  28. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/new_relic.py +129 -0
  29. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/pagerduty.py +74 -0
  30. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/snowflake.py +139 -0
  31. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/stripe.py +101 -0
  32. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/twilio.py +93 -0
  33. finops_mcp-0.3.0/finopsmcp/src/finops/connectors/saas/vercel.py +113 -0
  34. finops_mcp-0.3.0/finopsmcp/src/finops/integrations/__init__.py +0 -0
  35. finops_mcp-0.3.0/finopsmcp/src/finops/integrations/ticketing.py +286 -0
  36. finops_mcp-0.3.0/finopsmcp/src/finops/license.py +177 -0
  37. finops_mcp-0.3.0/finopsmcp/src/finops/notifications/__init__.py +0 -0
  38. finops_mcp-0.3.0/finopsmcp/src/finops/notifications/email_digest.py +176 -0
  39. finops_mcp-0.3.0/finopsmcp/src/finops/notifications/slack.py +142 -0
  40. finops_mcp-0.3.0/finopsmcp/src/finops/notifications/teams.py +163 -0
  41. finops_mcp-0.3.0/finopsmcp/src/finops/recommendations/__init__.py +0 -0
  42. finops_mcp-0.3.0/finopsmcp/src/finops/recommendations/commitments.py +275 -0
  43. finops_mcp-0.3.0/finopsmcp/src/finops/recommendations/rightsizing.py +236 -0
  44. finops_mcp-0.3.0/finopsmcp/src/finops/scheduler/__init__.py +0 -0
  45. finops_mcp-0.3.0/finopsmcp/src/finops/scheduler/jobs.py +386 -0
  46. finops_mcp-0.3.0/finopsmcp/src/finops/security/__init__.py +0 -0
  47. finops_mcp-0.3.0/finopsmcp/src/finops/security/env.py +52 -0
  48. finops_mcp-0.3.0/finopsmcp/src/finops/security/oauth/__init__.py +0 -0
  49. finops_mcp-0.3.0/finopsmcp/src/finops/security/oauth/aws.py +121 -0
  50. finops_mcp-0.3.0/finopsmcp/src/finops/security/oauth/azure.py +86 -0
  51. finops_mcp-0.3.0/finopsmcp/src/finops/security/oauth/gcp.py +100 -0
  52. finops_mcp-0.3.0/finopsmcp/src/finops/security/vault.py +262 -0
  53. finops_mcp-0.3.0/finopsmcp/src/finops/server.py +1056 -0
  54. finops_mcp-0.3.0/finopsmcp/src/finops/setup_wizard.py +337 -0
  55. finops_mcp-0.3.0/finopsmcp/src/finops/storage/__init__.py +0 -0
  56. finops_mcp-0.3.0/finopsmcp/src/finops/storage/db.py +108 -0
  57. finops_mcp-0.3.0/finopsmcp/src/finops/storage/snapshots.py +153 -0
  58. finops_mcp-0.3.0/finopsmcp/web/docs.html +798 -0
  59. finops_mcp-0.3.0/finopsmcp/web/index.html +847 -0
  60. finops_mcp-0.3.0/pyproject.toml +45 -0
  61. finops_mcp-0.3.0/src/finops/__init__.py +0 -0
  62. finops_mcp-0.3.0/src/finops/anomaly/__init__.py +0 -0
  63. finops_mcp-0.3.0/src/finops/anomaly/detector.py +170 -0
  64. finops_mcp-0.3.0/src/finops/anomaly/seasonality.py +131 -0
  65. finops_mcp-0.3.0/src/finops/attribution/__init__.py +0 -0
  66. finops_mcp-0.3.0/src/finops/attribution/fetcher.py +211 -0
  67. finops_mcp-0.3.0/src/finops/attribution/mapper.py +160 -0
  68. finops_mcp-0.3.0/src/finops/connectors/__init__.py +0 -0
  69. finops_mcp-0.3.0/src/finops/connectors/aws.py +176 -0
  70. finops_mcp-0.3.0/src/finops/connectors/azure.py +142 -0
  71. finops_mcp-0.3.0/src/finops/connectors/base.py +54 -0
  72. finops_mcp-0.3.0/src/finops/connectors/gcp.py +165 -0
  73. finops_mcp-0.3.0/src/finops/connectors/invoice/__init__.py +0 -0
  74. finops_mcp-0.3.0/src/finops/connectors/invoice/parser.py +324 -0
  75. finops_mcp-0.3.0/src/finops/connectors/saas/__init__.py +0 -0
  76. finops_mcp-0.3.0/src/finops/connectors/saas/cloudflare.py +102 -0
  77. finops_mcp-0.3.0/src/finops/connectors/saas/datadog.py +97 -0
  78. finops_mcp-0.3.0/src/finops/connectors/saas/github.py +133 -0
  79. finops_mcp-0.3.0/src/finops/connectors/saas/mongodb_atlas.py +105 -0
  80. finops_mcp-0.3.0/src/finops/connectors/saas/new_relic.py +129 -0
  81. finops_mcp-0.3.0/src/finops/connectors/saas/pagerduty.py +74 -0
  82. finops_mcp-0.3.0/src/finops/connectors/saas/snowflake.py +139 -0
  83. finops_mcp-0.3.0/src/finops/connectors/saas/stripe.py +101 -0
  84. finops_mcp-0.3.0/src/finops/connectors/saas/twilio.py +93 -0
  85. finops_mcp-0.3.0/src/finops/connectors/saas/vercel.py +113 -0
  86. finops_mcp-0.3.0/src/finops/integrations/__init__.py +0 -0
  87. finops_mcp-0.3.0/src/finops/integrations/ticketing.py +286 -0
  88. finops_mcp-0.3.0/src/finops/license.py +209 -0
  89. finops_mcp-0.3.0/src/finops/notifications/__init__.py +0 -0
  90. finops_mcp-0.3.0/src/finops/notifications/email_digest.py +176 -0
  91. finops_mcp-0.3.0/src/finops/notifications/slack.py +142 -0
  92. finops_mcp-0.3.0/src/finops/notifications/teams.py +163 -0
  93. finops_mcp-0.3.0/src/finops/recommendations/__init__.py +0 -0
  94. finops_mcp-0.3.0/src/finops/recommendations/commitments.py +275 -0
  95. finops_mcp-0.3.0/src/finops/recommendations/rightsizing.py +236 -0
  96. finops_mcp-0.3.0/src/finops/scheduler/__init__.py +0 -0
  97. finops_mcp-0.3.0/src/finops/scheduler/jobs.py +386 -0
  98. finops_mcp-0.3.0/src/finops/security/__init__.py +0 -0
  99. finops_mcp-0.3.0/src/finops/security/env.py +52 -0
  100. finops_mcp-0.3.0/src/finops/security/oauth/__init__.py +0 -0
  101. finops_mcp-0.3.0/src/finops/security/oauth/aws.py +121 -0
  102. finops_mcp-0.3.0/src/finops/security/oauth/azure.py +86 -0
  103. finops_mcp-0.3.0/src/finops/security/oauth/gcp.py +100 -0
  104. finops_mcp-0.3.0/src/finops/security/vault.py +262 -0
  105. finops_mcp-0.3.0/src/finops/server.py +1060 -0
  106. finops_mcp-0.3.0/src/finops/setup_wizard.py +337 -0
  107. finops_mcp-0.3.0/src/finops/storage/__init__.py +0 -0
  108. finops_mcp-0.3.0/src/finops/storage/db.py +108 -0
  109. finops_mcp-0.3.0/src/finops/storage/snapshots.py +153 -0
  110. finops_mcp-0.3.0/web/docs.html +798 -0
  111. finops_mcp-0.3.0/web/index.html +820 -0
  112. finops_mcp-0.3.0/web/vercel.json +5 -0
@@ -0,0 +1,74 @@
1
+ # ══════════════════════════════════════════════════════════════════════════════
2
+ # FinOps MCP — Environment Variables
3
+ # Copy to .env and fill in the credentials for the providers you use.
4
+ # Unconfigured providers are silently skipped.
5
+ # ══════════════════════════════════════════════════════════════════════════════
6
+
7
+ # ── AWS ────────────────────────────────────────────────────────────────────────
8
+ AWS_ACCESS_KEY_ID=
9
+ AWS_SECRET_ACCESS_KEY=
10
+ AWS_DEFAULT_REGION=us-east-1
11
+ # Multi-account: comma-separated IAM role ARNs to assume
12
+ # AWS_ROLE_ARNS=arn:aws:iam::111111111111:role/FinOps,arn:aws:iam::222222222222:role/FinOps
13
+
14
+ # ── Azure ──────────────────────────────────────────────────────────────────────
15
+ AZURE_CLIENT_ID=
16
+ AZURE_CLIENT_SECRET=
17
+ AZURE_TENANT_ID=
18
+ AZURE_SUBSCRIPTION_IDS= # comma-separated
19
+
20
+ # ── GCP ────────────────────────────────────────────────────────────────────────
21
+ GCP_SERVICE_ACCOUNT_KEY_PATH=
22
+ GCP_BILLING_ACCOUNT_IDS= # format: XXXXXX-XXXXXX-XXXXXX, comma-separated
23
+ # Optional: BigQuery billing export table for full granularity
24
+ # GCP_BQ_BILLING_TABLE=myproject.my_dataset.gcp_billing_export_v1_XXXXXX
25
+
26
+ # ── Datadog ────────────────────────────────────────────────────────────────────
27
+ DATADOG_API_KEY=
28
+ DATADOG_APP_KEY=
29
+ # DATADOG_SITE=datadoghq.com # use datadoghq.eu for EU
30
+
31
+ # ── Snowflake ──────────────────────────────────────────────────────────────────
32
+ SNOWFLAKE_ACCOUNT= # e.g. xy12345.us-east-1
33
+ SNOWFLAKE_USER=
34
+ SNOWFLAKE_PASSWORD=
35
+ SNOWFLAKE_WAREHOUSE= # optional, uses default if unset
36
+ SNOWFLAKE_ROLE=ACCOUNTADMIN
37
+ # SNOWFLAKE_PRIVATE_KEY_PATH= # alternative to password auth
38
+ # SNOWFLAKE_CREDIT_PRICE=3.00 # USD per credit (override if on a committed contract)
39
+
40
+ # ── GitHub ─────────────────────────────────────────────────────────────────────
41
+ GITHUB_TOKEN= # fine-grained PAT with read:org and read:billing
42
+ GITHUB_ORGS= # comma-separated org names
43
+
44
+ # ── Stripe ─────────────────────────────────────────────────────────────────────
45
+ STRIPE_SECRET_KEY= # sk_live_...
46
+
47
+ # ── MongoDB Atlas ──────────────────────────────────────────────────────────────
48
+ MONGODB_ATLAS_PUBLIC_KEY=
49
+ MONGODB_ATLAS_PRIVATE_KEY=
50
+ MONGODB_ATLAS_ORG_IDS= # comma-separated org IDs
51
+
52
+ # ── Vercel ─────────────────────────────────────────────────────────────────────
53
+ VERCEL_TOKEN=
54
+ VERCEL_TEAM_ID= # optional, leave blank for personal accounts
55
+
56
+ # ── Cloudflare ─────────────────────────────────────────────────────────────────
57
+ CLOUDFLARE_API_TOKEN=
58
+ CLOUDFLARE_ACCOUNT_ID=
59
+
60
+ # ── PagerDuty ──────────────────────────────────────────────────────────────────
61
+ PAGERDUTY_API_KEY=
62
+ # PAGERDUTY_PRICE_PER_USER=21.00 # override if on a negotiated contract
63
+
64
+ # ── Twilio ─────────────────────────────────────────────────────────────────────
65
+ TWILIO_ACCOUNT_SID=
66
+ TWILIO_AUTH_TOKEN=
67
+
68
+ # ── New Relic ──────────────────────────────────────────────────────────────────
69
+ NEW_RELIC_API_KEY= # User API key (NRAK-...)
70
+ NEW_RELIC_ACCOUNT_ID=
71
+ # NEW_RELIC_INGEST_PRICE_PER_GB=0.35 # override if on a negotiated contract
72
+
73
+ # ── General ────────────────────────────────────────────────────────────────────
74
+ DEFAULT_LOOKBACK_DAYS=30
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ *.egg
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Environment & secrets — NEVER commit these
19
+ .env
20
+ *.env
21
+ .env.local
22
+ .env.*.local
23
+ finops_vault.db
24
+ finops.db
25
+ *.db
26
+ *.sqlite
27
+
28
+ # Credentials & keys
29
+ *.pem
30
+ *.key
31
+ *.p12
32
+ *.pfx
33
+ service_account*.json
34
+ *credentials*.json
35
+
36
+ # Claude / MCP local state
37
+ .claude/
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+
43
+ # IDE
44
+ .vscode/
45
+ .idea/
46
+ *.swp
47
+ *.swo
48
+
49
+ # Logs
50
+ *.log
51
+ logs/
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: finops-mcp
3
+ Version: 0.3.0
4
+ Summary: Headless FinOps MCP server — cloud + SaaS cost intelligence for Claude
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: apscheduler>=3.10.0
7
+ Requires-Dist: azure-identity>=1.15.0
8
+ Requires-Dist: azure-mgmt-costmanagement>=4.0.0
9
+ Requires-Dist: boto3>=1.34.0
10
+ Requires-Dist: cryptography>=42.0.0
11
+ Requires-Dist: google-cloud-bigquery>=3.17.0
12
+ Requires-Dist: google-cloud-billing>=1.13.0
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: mcp[cli]>=1.3.0
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: python-dotenv>=1.0.0
17
+ Requires-Dist: pyyaml>=6.0.0
18
+ Requires-Dist: sqlalchemy>=2.0.0
19
+ Provides-Extra: azure-oauth
20
+ Requires-Dist: msal>=1.28.0; extra == 'azure-oauth'
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.4; extra == 'dev'
25
+ Provides-Extra: keyring
26
+ Requires-Dist: keyring>=25.0.0; extra == 'keyring'
27
+ Provides-Extra: pdf
28
+ Requires-Dist: pdfplumber>=0.10.0; extra == 'pdf'
29
+ Provides-Extra: snowflake
30
+ Requires-Dist: snowflake-connector-python>=3.6.0; extra == 'snowflake'
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "finops": {
4
+ "command": "/Users/chandan/finops/.venv/bin/python",
5
+ "args": ["-m", "finops.server"],
6
+ "cwd": "/Users/chandan/finops",
7
+ "env": {
8
+ "AWS_DEFAULT_REGION": "us-east-1"
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,74 @@
1
+ # ══════════════════════════════════════════════════════════════════════════════
2
+ # FinOps MCP — Environment Variables
3
+ # Copy to .env and fill in the credentials for the providers you use.
4
+ # Unconfigured providers are silently skipped.
5
+ # ══════════════════════════════════════════════════════════════════════════════
6
+
7
+ # ── AWS ────────────────────────────────────────────────────────────────────────
8
+ AWS_ACCESS_KEY_ID=
9
+ AWS_SECRET_ACCESS_KEY=
10
+ AWS_DEFAULT_REGION=us-east-1
11
+ # Multi-account: comma-separated IAM role ARNs to assume
12
+ # AWS_ROLE_ARNS=arn:aws:iam::111111111111:role/FinOps,arn:aws:iam::222222222222:role/FinOps
13
+
14
+ # ── Azure ──────────────────────────────────────────────────────────────────────
15
+ AZURE_CLIENT_ID=
16
+ AZURE_CLIENT_SECRET=
17
+ AZURE_TENANT_ID=
18
+ AZURE_SUBSCRIPTION_IDS= # comma-separated
19
+
20
+ # ── GCP ────────────────────────────────────────────────────────────────────────
21
+ GCP_SERVICE_ACCOUNT_KEY_PATH=
22
+ GCP_BILLING_ACCOUNT_IDS= # format: XXXXXX-XXXXXX-XXXXXX, comma-separated
23
+ # Optional: BigQuery billing export table for full granularity
24
+ # GCP_BQ_BILLING_TABLE=myproject.my_dataset.gcp_billing_export_v1_XXXXXX
25
+
26
+ # ── Datadog ────────────────────────────────────────────────────────────────────
27
+ DATADOG_API_KEY=
28
+ DATADOG_APP_KEY=
29
+ # DATADOG_SITE=datadoghq.com # use datadoghq.eu for EU
30
+
31
+ # ── Snowflake ──────────────────────────────────────────────────────────────────
32
+ SNOWFLAKE_ACCOUNT= # e.g. xy12345.us-east-1
33
+ SNOWFLAKE_USER=
34
+ SNOWFLAKE_PASSWORD=
35
+ SNOWFLAKE_WAREHOUSE= # optional, uses default if unset
36
+ SNOWFLAKE_ROLE=ACCOUNTADMIN
37
+ # SNOWFLAKE_PRIVATE_KEY_PATH= # alternative to password auth
38
+ # SNOWFLAKE_CREDIT_PRICE=3.00 # USD per credit (override if on a committed contract)
39
+
40
+ # ── GitHub ─────────────────────────────────────────────────────────────────────
41
+ GITHUB_TOKEN= # fine-grained PAT with read:org and read:billing
42
+ GITHUB_ORGS= # comma-separated org names
43
+
44
+ # ── Stripe ─────────────────────────────────────────────────────────────────────
45
+ STRIPE_SECRET_KEY= # sk_live_...
46
+
47
+ # ── MongoDB Atlas ──────────────────────────────────────────────────────────────
48
+ MONGODB_ATLAS_PUBLIC_KEY=
49
+ MONGODB_ATLAS_PRIVATE_KEY=
50
+ MONGODB_ATLAS_ORG_IDS= # comma-separated org IDs
51
+
52
+ # ── Vercel ─────────────────────────────────────────────────────────────────────
53
+ VERCEL_TOKEN=
54
+ VERCEL_TEAM_ID= # optional, leave blank for personal accounts
55
+
56
+ # ── Cloudflare ─────────────────────────────────────────────────────────────────
57
+ CLOUDFLARE_API_TOKEN=
58
+ CLOUDFLARE_ACCOUNT_ID=
59
+
60
+ # ── PagerDuty ──────────────────────────────────────────────────────────────────
61
+ PAGERDUTY_API_KEY=
62
+ # PAGERDUTY_PRICE_PER_USER=21.00 # override if on a negotiated contract
63
+
64
+ # ── Twilio ─────────────────────────────────────────────────────────────────────
65
+ TWILIO_ACCOUNT_SID=
66
+ TWILIO_AUTH_TOKEN=
67
+
68
+ # ── New Relic ──────────────────────────────────────────────────────────────────
69
+ NEW_RELIC_API_KEY= # User API key (NRAK-...)
70
+ NEW_RELIC_ACCOUNT_ID=
71
+ # NEW_RELIC_INGEST_PRICE_PER_GB=0.35 # override if on a negotiated contract
72
+
73
+ # ── General ────────────────────────────────────────────────────────────────────
74
+ DEFAULT_LOOKBACK_DAYS=30
@@ -0,0 +1,51 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ *.egg
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Environment & secrets — NEVER commit these
19
+ .env
20
+ *.env
21
+ .env.local
22
+ .env.*.local
23
+ finops_vault.db
24
+ finops.db
25
+ *.db
26
+ *.sqlite
27
+
28
+ # Credentials & keys
29
+ *.pem
30
+ *.key
31
+ *.p12
32
+ *.pfx
33
+ service_account*.json
34
+ *credentials*.json
35
+
36
+ # Claude / MCP local state
37
+ .claude/
38
+
39
+ # OS
40
+ .DS_Store
41
+ Thumbs.db
42
+
43
+ # IDE
44
+ .vscode/
45
+ .idea/
46
+ *.swp
47
+ *.swo
48
+
49
+ # Logs
50
+ *.log
51
+ logs/
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "finops": {
4
+ "command": "/Users/chandan/finops/.venv/bin/python",
5
+ "args": ["-m", "finops.server"],
6
+ "cwd": "/Users/chandan/finops",
7
+ "env": {
8
+ "AWS_DEFAULT_REGION": "us-east-1"
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "finops-mcp"
7
+ version = "0.3.0"
8
+ description = "Headless FinOps MCP server — cloud + SaaS cost intelligence for Claude"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "mcp[cli]>=1.3.0",
12
+ # cloud providers
13
+ "boto3>=1.34.0",
14
+ "azure-mgmt-costmanagement>=4.0.0",
15
+ "azure-identity>=1.15.0",
16
+ "google-cloud-billing>=1.13.0",
17
+ "google-cloud-bigquery>=3.17.0",
18
+ # storage + scheduling
19
+ "sqlalchemy>=2.0.0",
20
+ "apscheduler>=3.10.0",
21
+ # security
22
+ "cryptography>=42.0.0",
23
+ # shared utilities
24
+ "python-dotenv>=1.0.0",
25
+ "httpx>=0.27.0",
26
+ "pydantic>=2.0.0",
27
+ "pyyaml>=6.0.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ snowflake = ["snowflake-connector-python>=3.6.0"]
32
+ azure-oauth = ["msal>=1.28.0"]
33
+ keyring = ["keyring>=25.0.0"]
34
+ pdf = ["pdfplumber>=0.10.0"]
35
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff>=0.4"]
36
+
37
+ [project.scripts]
38
+ finops-mcp = "finops.server:main"
39
+ finops = "finops.setup_wizard:main"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/finops"]
43
+
44
+ [tool.ruff]
45
+ line-length = 100
File without changes
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import statistics
4
+ from dataclasses import dataclass, field
5
+ from datetime import date, datetime, timezone
6
+ from typing import Any
7
+
8
+ from sqlalchemy import and_, select
9
+
10
+ from ..storage.db import anomalies, get_engine
11
+ from ..storage.snapshots import get_history
12
+
13
+ _MIN_HISTORY_DAYS = 7 # need at least 7 data points
14
+ _MIN_SPEND_THRESHOLD = 5.0 # ignore noise below $5
15
+ _Z_SCORE_THRESHOLD = 2.0 # flag if |z| > 2.0
16
+ _PCT_THRESHOLD = 20.0 # AND |pct_change| > 20%
17
+
18
+
19
+ @dataclass
20
+ class AnomalyResult:
21
+ provider: str
22
+ service: str
23
+ account_id: str
24
+ snapshot_date: date
25
+ severity: str # "high" | "medium" | "low"
26
+ direction: str # "spike" | "drop"
27
+ pct_change: float
28
+ z_score: float
29
+ baseline_mean: float
30
+ current_amount: float
31
+ is_new: bool = True
32
+ metadata: dict[str, Any] = field(default_factory=dict)
33
+
34
+ def summary(self) -> str:
35
+ arrow = "↑" if self.direction == "spike" else "↓"
36
+ return (
37
+ f"{self.provider.upper()} / {self.service}: "
38
+ f"{arrow} {abs(self.pct_change):.0f}% vs 28-day baseline "
39
+ f"(${self.current_amount:,.2f} vs avg ${self.baseline_mean:,.2f}) "
40
+ f"[{self.severity.upper()}]"
41
+ )
42
+
43
+
44
+ def _severity(z: float, pct: float) -> str:
45
+ az, ap = abs(z), abs(pct)
46
+ if az >= 3.5 or ap >= 100:
47
+ return "high"
48
+ if az >= 2.5 or ap >= 50:
49
+ return "medium"
50
+ return "low"
51
+
52
+
53
+ def detect_for_series(
54
+ provider: str,
55
+ service: str,
56
+ account_id: str,
57
+ snapshot_date: date,
58
+ current_amount: float,
59
+ history_amounts: list[float],
60
+ ) -> AnomalyResult | None:
61
+ if len(history_amounts) < _MIN_HISTORY_DAYS:
62
+ return None
63
+ if current_amount < _MIN_SPEND_THRESHOLD and max(history_amounts, default=0) < _MIN_SPEND_THRESHOLD:
64
+ return None
65
+
66
+ mean = statistics.mean(history_amounts)
67
+ if mean < _MIN_SPEND_THRESHOLD:
68
+ return None
69
+
70
+ stdev = statistics.stdev(history_amounts) if len(history_amounts) > 1 else 0.0
71
+ z_score = (current_amount - mean) / stdev if stdev > 0 else 0.0
72
+ pct_change = (current_amount - mean) / mean * 100
73
+
74
+ if abs(z_score) < _Z_SCORE_THRESHOLD or abs(pct_change) < _PCT_THRESHOLD:
75
+ return None
76
+
77
+ return AnomalyResult(
78
+ provider=provider,
79
+ service=service,
80
+ account_id=account_id,
81
+ snapshot_date=snapshot_date,
82
+ severity=_severity(z_score, pct_change),
83
+ direction="spike" if pct_change > 0 else "drop",
84
+ pct_change=round(pct_change, 2),
85
+ z_score=round(z_score, 3),
86
+ baseline_mean=round(mean, 4),
87
+ current_amount=round(current_amount, 4),
88
+ )
89
+
90
+
91
+ def detect_from_snapshot(
92
+ provider: str,
93
+ service: str,
94
+ account_id: str,
95
+ snapshot_date: date,
96
+ current_amount: float,
97
+ lookback_days: int = 28,
98
+ ) -> AnomalyResult | None:
99
+ history = get_history(provider, service, account_id, days=lookback_days)
100
+ today_iso = snapshot_date.isoformat()
101
+ amounts = [
102
+ row["amount_usd"]
103
+ for row in history
104
+ if row["snapshot_date"] != today_iso and row["amount_usd"] > 0
105
+ ]
106
+ return detect_for_series(provider, service, account_id, snapshot_date, current_amount, amounts)
107
+
108
+
109
+ def persist_anomaly(result: AnomalyResult) -> int:
110
+ engine = get_engine()
111
+ with engine.begin() as conn:
112
+ r = conn.execute(
113
+ anomalies.insert().values(
114
+ provider=result.provider,
115
+ service=result.service,
116
+ account_id=result.account_id,
117
+ detected_at=datetime.now(timezone.utc),
118
+ snapshot_date=result.snapshot_date.isoformat(),
119
+ severity=result.severity,
120
+ direction=result.direction,
121
+ pct_change=result.pct_change,
122
+ z_score=result.z_score,
123
+ baseline_mean=result.baseline_mean,
124
+ current_amount=result.current_amount,
125
+ acknowledged=False,
126
+ notified=False,
127
+ )
128
+ )
129
+ return r.lastrowid # type: ignore[return-value]
130
+
131
+
132
+ def get_active_anomalies(
133
+ provider: str | None = None,
134
+ severity: str | None = None,
135
+ limit: int = 50,
136
+ ) -> list[dict[str, Any]]:
137
+ engine = get_engine()
138
+ query = (
139
+ select(anomalies)
140
+ .where(anomalies.c.acknowledged == False) # noqa: E712
141
+ .order_by(anomalies.c.detected_at.desc())
142
+ .limit(limit)
143
+ )
144
+ if provider:
145
+ query = query.where(anomalies.c.provider == provider)
146
+ if severity:
147
+ query = query.where(anomalies.c.severity == severity)
148
+ with engine.connect() as conn:
149
+ return [dict(r._mapping) for r in conn.execute(query).fetchall()]
150
+
151
+
152
+ def acknowledge_anomaly(anomaly_id: int) -> bool:
153
+ engine = get_engine()
154
+ with engine.begin() as conn:
155
+ result = conn.execute(
156
+ anomalies.update()
157
+ .where(anomalies.c.id == anomaly_id)
158
+ .values(acknowledged=True)
159
+ )
160
+ return result.rowcount > 0
161
+
162
+
163
+ def mark_notified(anomaly_id: int) -> None:
164
+ engine = get_engine()
165
+ with engine.begin() as conn:
166
+ conn.execute(
167
+ anomalies.update()
168
+ .where(anomalies.c.id == anomaly_id)
169
+ .values(notified=True)
170
+ )
@@ -0,0 +1,131 @@
1
+ """
2
+ Seasonality-aware anomaly detection.
3
+
4
+ Compares today against the same weekday over the prior N weeks, not a flat
5
+ rolling mean. This eliminates false positives from weekly patterns (e.g.
6
+ Monday batch jobs, weekend traffic drops) that naive z-score flags as anomalies.
7
+
8
+ Strategy:
9
+ 1. Same-weekday baseline: compare Monday vs last 4 Mondays, etc.
10
+ 2. If not enough same-weekday points, fall back to rolling-mean detection.
11
+ 3. Combine both signals — flag only when both agree (reduces false positives).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import statistics
16
+ from datetime import date, timedelta
17
+ from typing import Any
18
+
19
+ from .detector import (
20
+ AnomalyResult,
21
+ _MIN_HISTORY_DAYS,
22
+ _MIN_SPEND_THRESHOLD,
23
+ _PCT_THRESHOLD,
24
+ _Z_SCORE_THRESHOLD,
25
+ _severity,
26
+ detect_for_series,
27
+ )
28
+ from ..storage.snapshots import get_history
29
+
30
+ _MIN_SAME_WEEKDAY_POINTS = 3 # need at least 3 same-weekday readings
31
+
32
+
33
+ def _same_weekday_amounts(
34
+ history: list[dict[str, Any]],
35
+ target_weekday: int, # 0=Monday … 6=Sunday
36
+ current_date_iso: str,
37
+ ) -> list[float]:
38
+ """Return amounts from history rows that fall on target_weekday, excluding today."""
39
+ out: list[float] = []
40
+ for row in history:
41
+ if row["snapshot_date"] == current_date_iso:
42
+ continue
43
+ try:
44
+ d = date.fromisoformat(row["snapshot_date"])
45
+ except ValueError:
46
+ continue
47
+ if d.weekday() == target_weekday and row["amount_usd"] > 0:
48
+ out.append(row["amount_usd"])
49
+ return out
50
+
51
+
52
+ def detect_with_seasonality(
53
+ provider: str,
54
+ service: str,
55
+ account_id: str,
56
+ snapshot_date: date,
57
+ current_amount: float,
58
+ lookback_days: int = 56, # 8 weeks — enough for 8 same-weekday samples
59
+ ) -> AnomalyResult | None:
60
+ """
61
+ Seasonality-aware detection. Lookback extended to 56 days (8 weeks) to
62
+ collect enough same-weekday readings; falls back to rolling mean if sparse.
63
+ """
64
+ if current_amount < _MIN_SPEND_THRESHOLD:
65
+ return None
66
+
67
+ history = get_history(provider, service, account_id, days=lookback_days)
68
+ today_iso = snapshot_date.isoformat()
69
+ weekday = snapshot_date.weekday()
70
+
71
+ same_day_amounts = _same_weekday_amounts(history, weekday, today_iso)
72
+
73
+ if len(same_day_amounts) >= _MIN_SAME_WEEKDAY_POINTS:
74
+ result = _detect_against_baseline(
75
+ provider, service, account_id, snapshot_date, current_amount,
76
+ same_day_amounts, baseline_label="same-weekday"
77
+ )
78
+ if result is not None:
79
+ result.metadata["detection_method"] = "seasonality-aware (same-weekday)"
80
+ result.metadata["weekday_samples"] = len(same_day_amounts)
81
+ return result
82
+ else:
83
+ # Fallback: classic rolling-mean (28-day)
84
+ rolling_amounts = [
85
+ row["amount_usd"]
86
+ for row in history
87
+ if row["snapshot_date"] != today_iso and row["amount_usd"] > 0
88
+ ]
89
+ result = detect_for_series(
90
+ provider, service, account_id, snapshot_date,
91
+ current_amount, rolling_amounts
92
+ )
93
+ if result is not None:
94
+ result.metadata["detection_method"] = "rolling-mean (insufficient same-weekday data)"
95
+ result.metadata["weekday_samples"] = len(same_day_amounts)
96
+ return result
97
+
98
+
99
+ def _detect_against_baseline(
100
+ provider: str,
101
+ service: str,
102
+ account_id: str,
103
+ snapshot_date: date,
104
+ current_amount: float,
105
+ baseline_amounts: list[float],
106
+ baseline_label: str,
107
+ ) -> AnomalyResult | None:
108
+ mean = statistics.mean(baseline_amounts)
109
+ if mean < _MIN_SPEND_THRESHOLD:
110
+ return None
111
+
112
+ stdev = statistics.stdev(baseline_amounts) if len(baseline_amounts) > 1 else 0.0
113
+ z_score = (current_amount - mean) / stdev if stdev > 0 else 0.0
114
+ pct_change = (current_amount - mean) / mean * 100
115
+
116
+ if abs(z_score) < _Z_SCORE_THRESHOLD or abs(pct_change) < _PCT_THRESHOLD:
117
+ return None
118
+
119
+ return AnomalyResult(
120
+ provider=provider,
121
+ service=service,
122
+ account_id=account_id,
123
+ snapshot_date=snapshot_date,
124
+ severity=_severity(z_score, pct_change),
125
+ direction="spike" if pct_change > 0 else "drop",
126
+ pct_change=round(pct_change, 2),
127
+ z_score=round(z_score, 3),
128
+ baseline_mean=round(mean, 4),
129
+ current_amount=round(current_amount, 4),
130
+ metadata={"baseline": baseline_label},
131
+ )