buildshield-mcp 0.1.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.
@@ -0,0 +1,37 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ backend/venv/
5
+ backend/.env
6
+ backend/.env.local
7
+
8
+ # Node
9
+ frontend/node_modules/
10
+ frontend/.next/
11
+ frontend/.env.local
12
+ frontend/.env
13
+
14
+ # Env files (catch-all)
15
+ *.env
16
+ frontend/.playwright-cli/
17
+
18
+ # IDE
19
+ .vscode/
20
+ .idea/
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Worktrees
27
+ .worktrees/
28
+
29
+ # Claude-Flow dev tool state (local only)
30
+ .claude-flow/
31
+ .swarm/
32
+
33
+ # Local/test artifacts
34
+ *.json.bak
35
+ intense_chat_export.json
36
+ models_output.json
37
+ test_output.json
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildshield-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for BuildShield — AI cost intelligence for agents
5
+ Project-URL: Homepage, https://thebuildshield.com
6
+ Project-URL: Repository, https://github.com/Dev-31/theBuildShield.com
7
+ License: MIT
8
+ Keywords: ai,buildshield,cost,llm,mcp
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.27.0
11
+ Requires-Dist: mcp>=1.0.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # buildshield-mcp
15
+
16
+ MCP server for BuildShield — AI cost intelligence for agents.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install buildshield-mcp
22
+ ```
23
+
24
+ ## Configure (Claude Desktop / Cursor)
25
+
26
+ Add to your MCP config (`~/.config/claude/claude_desktop_config.json`):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "buildshield": {
32
+ "command": "buildshield-mcp",
33
+ "env": {
34
+ "BUILDSHIELD_API_KEY": "bsk_..."
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Get your API key at [thebuildshield.com/settings](https://thebuildshield.com/settings).
42
+
43
+ ## Tools
44
+
45
+ | Tool | Description | Requires key? |
46
+ |------|-------------|---------------|
47
+ | `buildshield_check_cost` | Exact cost for model + token count | No — offline |
48
+ | `buildshield_recommend_model` | Best models for a task with filters | No — offline |
49
+ | `buildshield_get_pricing` | Browse all model pricing | No — offline |
50
+ | `buildshield_optimize_prompt` | Rule-based prompt compression (20-40% savings) | No — offline |
51
+ | `buildshield_check_budget` | Check remaining budget for your API key | Yes |
52
+ | `buildshield_log_usage` | Log usage to your dashboard | Optional |
53
+
54
+ ## Examples
55
+
56
+ ```
57
+ "What does claude-sonnet-4.6 cost for 50K input + 10K output tokens?"
58
+ → buildshield_check_cost(model="claude-sonnet-4.6", input_tokens=50000, output_tokens=10000)
59
+
60
+ "What's the cheapest model with at least 200K context?"
61
+ → buildshield_recommend_model(task="long document analysis", min_context_tokens=200000)
62
+
63
+ "Compress this system prompt"
64
+ → buildshield_optimize_prompt(text="...", aggressiveness="medium")
65
+ ```
@@ -0,0 +1,52 @@
1
+ # buildshield-mcp
2
+
3
+ MCP server for BuildShield — AI cost intelligence for agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install buildshield-mcp
9
+ ```
10
+
11
+ ## Configure (Claude Desktop / Cursor)
12
+
13
+ Add to your MCP config (`~/.config/claude/claude_desktop_config.json`):
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "buildshield": {
19
+ "command": "buildshield-mcp",
20
+ "env": {
21
+ "BUILDSHIELD_API_KEY": "bsk_..."
22
+ }
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ Get your API key at [thebuildshield.com/settings](https://thebuildshield.com/settings).
29
+
30
+ ## Tools
31
+
32
+ | Tool | Description | Requires key? |
33
+ |------|-------------|---------------|
34
+ | `buildshield_check_cost` | Exact cost for model + token count | No — offline |
35
+ | `buildshield_recommend_model` | Best models for a task with filters | No — offline |
36
+ | `buildshield_get_pricing` | Browse all model pricing | No — offline |
37
+ | `buildshield_optimize_prompt` | Rule-based prompt compression (20-40% savings) | No — offline |
38
+ | `buildshield_check_budget` | Check remaining budget for your API key | Yes |
39
+ | `buildshield_log_usage` | Log usage to your dashboard | Optional |
40
+
41
+ ## Examples
42
+
43
+ ```
44
+ "What does claude-sonnet-4.6 cost for 50K input + 10K output tokens?"
45
+ → buildshield_check_cost(model="claude-sonnet-4.6", input_tokens=50000, output_tokens=10000)
46
+
47
+ "What's the cheapest model with at least 200K context?"
48
+ → buildshield_recommend_model(task="long document analysis", min_context_tokens=200000)
49
+
50
+ "Compress this system prompt"
51
+ → buildshield_optimize_prompt(text="...", aggressiveness="medium")
52
+ ```
@@ -0,0 +1,3 @@
1
+ """BuildShield MCP Server — AI cost intelligence for agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,232 @@
1
+ """
2
+ BuildShield MCP Server — stdio transport entry point.
3
+
4
+ Usage in Claude Desktop / Cursor / any MCP client:
5
+
6
+ {
7
+ "mcpServers": {
8
+ "buildshield": {
9
+ "command": "buildshield-mcp",
10
+ "env": { "BUILDSHIELD_API_KEY": "bsk_..." }
11
+ }
12
+ }
13
+ }
14
+
15
+ Or with npx (npm wrapper — add later):
16
+ "command": "npx", "args": ["-y", "buildshield-mcp"]
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import os
23
+ from typing import Any
24
+
25
+ from mcp.server import Server
26
+ from mcp.server.stdio import stdio_server
27
+ from mcp import types
28
+
29
+ from .tools import (
30
+ handle_check_cost,
31
+ handle_recommend_model,
32
+ handle_get_pricing,
33
+ handle_optimize_prompt,
34
+ handle_check_budget,
35
+ handle_log_usage,
36
+ )
37
+
38
+ app = Server("buildshield-mcp")
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Tool schemas
42
+ # ---------------------------------------------------------------------------
43
+
44
+ TOOLS: list[types.Tool] = [
45
+ types.Tool(
46
+ name="buildshield_check_cost",
47
+ description=(
48
+ "Calculate the exact USD cost for a specific model and token count. "
49
+ "Works offline — no API key needed. "
50
+ "Use this before making expensive LLM calls to verify cost."
51
+ ),
52
+ inputSchema={
53
+ "type": "object",
54
+ "properties": {
55
+ "model": {"type": "string", "description": "Model name, e.g. 'gpt-4.1', 'claude-sonnet-4.6', 'gemini-2.5-flash'"},
56
+ "input_tokens": {"type": "integer", "description": "Number of input/prompt tokens"},
57
+ "output_tokens": {"type": "integer", "description": "Number of output/completion tokens (default 0)", "default": 0},
58
+ },
59
+ "required": ["model", "input_tokens"],
60
+ },
61
+ ),
62
+ types.Tool(
63
+ name="buildshield_recommend_model",
64
+ description=(
65
+ "Get the best model recommendations for a task, filtered by cost and context window. "
66
+ "Works offline. Returns a ranked table with pricing."
67
+ ),
68
+ inputSchema={
69
+ "type": "object",
70
+ "properties": {
71
+ "task": {"type": "string", "description": "What you're using the model for, e.g. 'code review', 'summarization', 'complex reasoning'"},
72
+ "max_cost_per_1m_input": {"type": "number", "description": "Maximum acceptable input cost per 1M tokens (USD)"},
73
+ "min_context_tokens": {"type": "integer", "description": "Minimum context window required (tokens)"},
74
+ "provider": {"type": "string", "description": "Filter to a specific provider: openai, anthropic, google, deepseek"},
75
+ },
76
+ "required": ["task"],
77
+ },
78
+ ),
79
+ types.Tool(
80
+ name="buildshield_get_pricing",
81
+ description=(
82
+ "Get current pricing for one model or browse all models. "
83
+ "Works offline. Returns input/output price per 1M tokens, context window, and tier."
84
+ ),
85
+ inputSchema={
86
+ "type": "object",
87
+ "properties": {
88
+ "model": {"type": "string", "description": "Specific model to look up (optional — omit to see all models)"},
89
+ "provider": {"type": "string", "description": "Filter by provider: openai, anthropic, google, deepseek"},
90
+ },
91
+ },
92
+ ),
93
+ types.Tool(
94
+ name="buildshield_optimize_prompt",
95
+ description=(
96
+ "Compress a prompt using rule-based techniques to reduce token count by 20-40%. "
97
+ "No AI required — works completely offline. "
98
+ "Removes filler phrases, normalizes whitespace, deduplicates repeated instructions."
99
+ ),
100
+ inputSchema={
101
+ "type": "object",
102
+ "properties": {
103
+ "text": {"type": "string", "description": "The prompt text to optimize"},
104
+ "aggressiveness": {
105
+ "type": "string",
106
+ "enum": ["light", "medium", "heavy"],
107
+ "description": "Compression level: light=whitespace only, medium=filler phrases, heavy=deduplication (default: medium)",
108
+ "default": "medium",
109
+ },
110
+ },
111
+ "required": ["text"],
112
+ },
113
+ ),
114
+ types.Tool(
115
+ name="buildshield_check_budget",
116
+ description=(
117
+ "Check your remaining BuildShield budget for the current period. "
118
+ "Requires a BuildShield API key (bsk_...) from https://thebuildshield.com/settings."
119
+ ),
120
+ inputSchema={
121
+ "type": "object",
122
+ "properties": {
123
+ "api_key": {"type": "string", "description": "Your BuildShield API key (bsk_...). Falls back to BUILDSHIELD_API_KEY env var."},
124
+ "period": {"type": "string", "enum": ["today", "week", "month"], "description": "Budget period to check (default: today)", "default": "today"},
125
+ },
126
+ },
127
+ ),
128
+ types.Tool(
129
+ name="buildshield_log_usage",
130
+ description=(
131
+ "Log LLM usage to BuildShield for tracking and budget management. "
132
+ "Always calculates cost locally (offline). Syncs to dashboard if api_key provided."
133
+ ),
134
+ inputSchema={
135
+ "type": "object",
136
+ "properties": {
137
+ "model": {"type": "string", "description": "Model used, e.g. 'gpt-4.1'"},
138
+ "input_tokens": {"type": "integer", "description": "Input token count"},
139
+ "output_tokens": {"type": "integer", "description": "Output token count"},
140
+ "api_key": {"type": "string", "description": "BuildShield API key to sync to dashboard (optional)"},
141
+ "task_description": {"type": "string", "description": "Brief description of the task for your records (optional)"},
142
+ },
143
+ "required": ["model", "input_tokens", "output_tokens"],
144
+ },
145
+ ),
146
+ ]
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # MCP handlers
151
+ # ---------------------------------------------------------------------------
152
+
153
+ @app.list_tools()
154
+ async def list_tools() -> list[types.Tool]:
155
+ return TOOLS
156
+
157
+
158
+ @app.call_tool()
159
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
160
+ try:
161
+ if name == "buildshield_check_cost":
162
+ result = handle_check_cost(
163
+ model=arguments["model"],
164
+ input_tokens=int(arguments["input_tokens"]),
165
+ output_tokens=int(arguments.get("output_tokens", 0)),
166
+ )
167
+
168
+ elif name == "buildshield_recommend_model":
169
+ result = handle_recommend_model(
170
+ task=arguments["task"],
171
+ max_cost_per_1m_input=arguments.get("max_cost_per_1m_input"),
172
+ min_context_tokens=arguments.get("min_context_tokens"),
173
+ provider=arguments.get("provider"),
174
+ )
175
+
176
+ elif name == "buildshield_get_pricing":
177
+ result = handle_get_pricing(
178
+ model=arguments.get("model"),
179
+ provider=arguments.get("provider"),
180
+ )
181
+
182
+ elif name == "buildshield_optimize_prompt":
183
+ result = handle_optimize_prompt(
184
+ text=arguments["text"],
185
+ aggressiveness=arguments.get("aggressiveness", "medium"),
186
+ )
187
+
188
+ elif name == "buildshield_check_budget":
189
+ api_key = arguments.get("api_key") or os.environ.get("BUILDSHIELD_API_KEY")
190
+ result = await handle_check_budget(
191
+ api_key=api_key,
192
+ period=arguments.get("period", "today"),
193
+ )
194
+
195
+ elif name == "buildshield_log_usage":
196
+ api_key = arguments.get("api_key") or os.environ.get("BUILDSHIELD_API_KEY")
197
+ result = await handle_log_usage(
198
+ model=arguments["model"],
199
+ input_tokens=int(arguments["input_tokens"]),
200
+ output_tokens=int(arguments["output_tokens"]),
201
+ api_key=api_key,
202
+ task_description=arguments.get("task_description"),
203
+ )
204
+
205
+ else:
206
+ result = f"Unknown tool: {name}"
207
+
208
+ except Exception as e:
209
+ result = f"Error running {name}: {e}"
210
+
211
+ return [types.TextContent(type="text", text=result)]
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Entry point
216
+ # ---------------------------------------------------------------------------
217
+
218
+ async def main() -> None:
219
+ async with stdio_server() as (read_stream, write_stream):
220
+ await app.run(
221
+ read_stream,
222
+ write_stream,
223
+ app.create_initialization_options(),
224
+ )
225
+
226
+
227
+ def main_sync() -> None:
228
+ asyncio.run(main())
229
+
230
+
231
+ if __name__ == "__main__":
232
+ main_sync()
@@ -0,0 +1,546 @@
1
+ """
2
+ BuildShield MCP Tool Handlers
3
+
4
+ All offline tools (check_cost, recommend_model, get_pricing, optimize_prompt)
5
+ work with zero network calls using the embedded pricing table.
6
+
7
+ Online tools (check_budget, log_usage) require a BuildShield API key (bsk_...)
8
+ which users can obtain at https://thebuildshield.com.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Try to import the live pricing engine from the repo backend
20
+ # ---------------------------------------------------------------------------
21
+ _BACKEND_PATH = Path(__file__).parent.parent.parent.parent / "backend"
22
+ _PRICING_AVAILABLE = False
23
+
24
+ if _BACKEND_PATH.exists():
25
+ sys.path.insert(0, str(_BACKEND_PATH))
26
+ try:
27
+ from app.modules.llm_cost.pricing import ( # type: ignore[import]
28
+ get_pricing_table,
29
+ resolve_model,
30
+ calculate_cost,
31
+ )
32
+ _PRICING_AVAILABLE = True
33
+ except Exception:
34
+ pass
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Embedded fallback — used when running standalone outside the repo
38
+ # ---------------------------------------------------------------------------
39
+ _FALLBACK_MODELS: list[dict] = [
40
+ {"provider": "openai", "model": "gpt-4.1", "display_name": "GPT-4.1", "input_per_1m": 2.00, "output_per_1m": 8.00, "context": 1_000_000, "tier": "balanced"},
41
+ {"provider": "openai", "model": "gpt-4.1-mini", "display_name": "GPT-4.1 Mini", "input_per_1m": 0.40, "output_per_1m": 1.60, "context": 1_000_000, "tier": "cheap"},
42
+ {"provider": "openai", "model": "gpt-4o", "display_name": "GPT-4o", "input_per_1m": 2.50, "output_per_1m": 10.00, "context": 128_000, "tier": "balanced"},
43
+ {"provider": "openai", "model": "gpt-4o-mini", "display_name": "GPT-4o Mini", "input_per_1m": 0.15, "output_per_1m": 0.60, "context": 128_000, "tier": "cheap"},
44
+ {"provider": "openai", "model": "o1", "display_name": "o1", "input_per_1m": 15.00, "output_per_1m": 60.00, "context": 200_000, "tier": "premium"},
45
+ {"provider": "openai", "model": "o3-mini", "display_name": "o3-mini", "input_per_1m": 1.10, "output_per_1m": 4.40, "context": 200_000, "tier": "balanced"},
46
+ {"provider": "anthropic", "model": "claude-opus-4.6", "display_name": "Claude Opus 4.6", "input_per_1m": 15.00, "output_per_1m": 75.00, "context": 200_000, "tier": "premium"},
47
+ {"provider": "anthropic", "model": "claude-sonnet-4.6", "display_name": "Claude Sonnet 4.6", "input_per_1m": 3.00, "output_per_1m": 15.00, "context": 200_000, "tier": "balanced"},
48
+ {"provider": "anthropic", "model": "claude-sonnet-4.5", "display_name": "Claude Sonnet 4.5", "input_per_1m": 3.00, "output_per_1m": 15.00, "context": 200_000, "tier": "balanced"},
49
+ {"provider": "anthropic", "model": "claude-haiku-4.5", "display_name": "Claude Haiku 4.5", "input_per_1m": 1.00, "output_per_1m": 5.00, "context": 200_000, "tier": "cheap"},
50
+ {"provider": "google", "model": "gemini-2.5-pro", "display_name": "Gemini 2.5 Pro", "input_per_1m": 1.25, "output_per_1m": 10.00, "context": 1_000_000, "tier": "balanced"},
51
+ {"provider": "google", "model": "gemini-2.5-flash", "display_name": "Gemini 2.5 Flash", "input_per_1m": 0.30, "output_per_1m": 2.50, "context": 1_000_000, "tier": "cheap"},
52
+ {"provider": "google", "model": "gemini-2.0-flash", "display_name": "Gemini 2.0 Flash", "input_per_1m": 0.10, "output_per_1m": 0.40, "context": 1_000_000, "tier": "cheap"},
53
+ {"provider": "deepseek", "model": "deepseek-chat", "display_name": "DeepSeek Chat", "input_per_1m": 0.28, "output_per_1m": 0.42, "context": 128_000, "tier": "cheap"},
54
+ {"provider": "deepseek", "model": "deepseek-r1", "display_name": "DeepSeek R1", "input_per_1m": 0.55, "output_per_1m": 2.19, "context": 128_000, "tier": "cheap"},
55
+ ]
56
+
57
+ _API_BASE = "https://thebuildshieldcom-production.up.railway.app"
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Helpers
61
+ # ---------------------------------------------------------------------------
62
+
63
+ def _fmt_tokens(n: int) -> str:
64
+ if n >= 1_000_000:
65
+ return f"{n/1_000_000:.1f}M"
66
+ if n >= 1_000:
67
+ return f"{n/1_000:.0f}K"
68
+ return str(n)
69
+
70
+
71
+ def _get_fallback_table() -> list[dict]:
72
+ return _FALLBACK_MODELS
73
+
74
+
75
+ def _fallback_resolve(model_name: str) -> dict | None:
76
+ name_lower = model_name.lower().strip()
77
+ for m in _FALLBACK_MODELS:
78
+ if m["model"] == name_lower or m["display_name"].lower() == name_lower:
79
+ return m
80
+ # Partial match
81
+ for m in _FALLBACK_MODELS:
82
+ if name_lower in m["model"] or m["model"] in name_lower:
83
+ return m
84
+ return None
85
+
86
+
87
+ def _fallback_calculate(model_name: str, input_tokens: int, output_tokens: int) -> float | None:
88
+ m = _fallback_resolve(model_name)
89
+ if not m:
90
+ return None
91
+ return (input_tokens / 1_000_000) * m["input_per_1m"] + (output_tokens / 1_000_000) * m["output_per_1m"]
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Tool 1: buildshield_check_cost
96
+ # ---------------------------------------------------------------------------
97
+
98
+ def handle_check_cost(model: str, input_tokens: int, output_tokens: int = 0) -> str:
99
+ """Calculate exact cost for a specific model and token count. Works offline."""
100
+ if _PRICING_AVAILABLE:
101
+ mp = resolve_model(model)
102
+ if mp:
103
+ input_cost = (input_tokens / 1_000_000) * mp.input_per_1m
104
+ output_cost = (output_tokens / 1_000_000) * mp.output_per_1m
105
+ total = input_cost + output_cost
106
+ return (
107
+ f"Model: {mp.display_name} ({mp.provider})\n"
108
+ f"Tier: {mp.family}\n"
109
+ f"Input: {_fmt_tokens(input_tokens)} tokens × ${mp.input_per_1m:.2f}/1M = ${input_cost:.6f}\n"
110
+ f"Output: {_fmt_tokens(output_tokens)} tokens × ${mp.output_per_1m:.2f}/1M = ${output_cost:.6f}\n"
111
+ f"Total: ${total:.6f} (${total*100:.4f}¢)\n"
112
+ f"\nContext window: {_fmt_tokens(mp.max_context_tokens)}"
113
+ )
114
+ else:
115
+ m = _fallback_resolve(model)
116
+ if m:
117
+ input_cost = (input_tokens / 1_000_000) * m["input_per_1m"]
118
+ output_cost = (output_tokens / 1_000_000) * m["output_per_1m"]
119
+ total = input_cost + output_cost
120
+ return (
121
+ f"Model: {m['display_name']} ({m['provider']})\n"
122
+ f"Tier: {m['tier']}\n"
123
+ f"Input: {_fmt_tokens(input_tokens)} tokens × ${m['input_per_1m']:.2f}/1M = ${input_cost:.6f}\n"
124
+ f"Output: {_fmt_tokens(output_tokens)} tokens × ${m['output_per_1m']:.2f}/1M = ${output_cost:.6f}\n"
125
+ f"Total: ${total:.6f} (${total*100:.4f}¢)\n"
126
+ f"\nContext window: {_fmt_tokens(m['context'])}"
127
+ )
128
+
129
+ return (
130
+ f"Model '{model}' not found in BuildShield pricing registry.\n"
131
+ f"Try: gpt-4.1, gpt-4o, claude-sonnet-4.6, gemini-2.5-flash, deepseek-chat\n"
132
+ f"Or use buildshield_get_pricing to browse all available models."
133
+ )
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Tool 2: buildshield_recommend_model
138
+ # ---------------------------------------------------------------------------
139
+
140
+ def handle_recommend_model(
141
+ task: str,
142
+ max_cost_per_1m_input: float | None = None,
143
+ min_context_tokens: int | None = None,
144
+ provider: str | None = None,
145
+ ) -> str:
146
+ """Recommend the best models for a task based on cost and capability filters. Works offline."""
147
+ if _PRICING_AVAILABLE:
148
+ table = get_pricing_table()
149
+ candidates = list(table.values())
150
+
151
+ def to_dict(mp: Any) -> dict:
152
+ return {
153
+ "model": mp.model,
154
+ "display_name": mp.display_name,
155
+ "provider": mp.provider,
156
+ "input_per_1m": mp.input_per_1m,
157
+ "output_per_1m": mp.output_per_1m,
158
+ "context": mp.max_context_tokens,
159
+ "tier": mp.family,
160
+ "status": mp.status,
161
+ }
162
+
163
+ models = [to_dict(m) for m in candidates if m.status != "legacy"]
164
+ else:
165
+ models = [m for m in _FALLBACK_MODELS]
166
+
167
+ # Apply filters
168
+ if max_cost_per_1m_input is not None:
169
+ models = [m for m in models if m["input_per_1m"] <= max_cost_per_1m_input]
170
+ if min_context_tokens is not None:
171
+ models = [m for m in models if m["context"] >= min_context_tokens]
172
+ if provider:
173
+ models = [m for m in models if m["provider"].lower() == provider.lower()]
174
+
175
+ # Sort by input cost
176
+ models.sort(key=lambda m: m["input_per_1m"])
177
+
178
+ if not models:
179
+ return (
180
+ "No models match your filters.\n"
181
+ "Try relaxing: max_cost_per_1m_input or min_context_tokens.\n"
182
+ "Use buildshield_get_pricing to see all available models."
183
+ )
184
+
185
+ # Build recommendation
186
+ lines = [f"Top model recommendations for: {task}\n"]
187
+ lines.append(f"{'Model':<30} {'Provider':<12} {'Input/1M':>10} {'Output/1M':>11} {'Context':>10} {'Tier':<10}")
188
+ lines.append("─" * 85)
189
+
190
+ for m in models[:6]:
191
+ ctx = _fmt_tokens(m["context"])
192
+ lines.append(
193
+ f"{m['display_name']:<30} {m['provider']:<12} ${m['input_per_1m']:>8.2f} "
194
+ f"${m['output_per_1m']:>9.2f} {ctx:>10} {m['tier']:<10}"
195
+ )
196
+
197
+ # Add a recommendation note based on task keywords
198
+ task_lower = task.lower()
199
+ note = ""
200
+ if any(w in task_lower for w in ["summar", "classif", "extract", "simple", "short"]):
201
+ note = "\n💡 For simple extraction/classification tasks, consider the cheapest option — quality difference is minimal."
202
+ elif any(w in task_lower for w in ["code", "debug", "program", "function"]):
203
+ note = "\n💡 For coding tasks, balanced-tier models (Sonnet, GPT-4.1) offer the best quality/cost ratio."
204
+ elif any(w in task_lower for w in ["reason", "complex", "analyz", "research"]):
205
+ note = "\n💡 For complex reasoning, premium models (Opus, o1) justify their cost with significantly better output."
206
+ elif any(w in task_lower for w in ["long", "document", "book", "context"]):
207
+ note = "\n💡 For long-context tasks, Gemini 2.5 Pro (1M context) or Claude (200K) are the best choices."
208
+
209
+ return "\n".join(lines) + note
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Tool 3: buildshield_get_pricing
214
+ # ---------------------------------------------------------------------------
215
+
216
+ def handle_get_pricing(model: str | None = None, provider: str | None = None) -> str:
217
+ """Get pricing for one model or browse all models. Works offline."""
218
+ if model:
219
+ if _PRICING_AVAILABLE:
220
+ mp = resolve_model(model)
221
+ if mp:
222
+ return (
223
+ f"Model: {mp.display_name}\n"
224
+ f"Provider: {mp.provider}\n"
225
+ f"Model ID: {mp.model}\n"
226
+ f"Input price: ${mp.input_per_1m:.2f} per 1M tokens\n"
227
+ f"Output price: ${mp.output_per_1m:.2f} per 1M tokens\n"
228
+ f"Context: {_fmt_tokens(mp.max_context_tokens)}\n"
229
+ f"Tier: {mp.family}\n"
230
+ f"Status: {mp.status}\n"
231
+ + (f"Alternatives: {', '.join(mp.alternatives)}" if mp.alternatives else "")
232
+ )
233
+ else:
234
+ m = _fallback_resolve(model)
235
+ if m:
236
+ return (
237
+ f"Model: {m['display_name']}\n"
238
+ f"Provider: {m['provider']}\n"
239
+ f"Model ID: {m['model']}\n"
240
+ f"Input price: ${m['input_per_1m']:.2f} per 1M tokens\n"
241
+ f"Output price: ${m['output_per_1m']:.2f} per 1M tokens\n"
242
+ f"Context: {_fmt_tokens(m['context'])}\n"
243
+ f"Tier: {m['tier']}"
244
+ )
245
+ return f"Model '{model}' not found. Use buildshield_get_pricing (no model arg) to see all models."
246
+
247
+ # Return full table
248
+ if _PRICING_AVAILABLE:
249
+ table = get_pricing_table()
250
+ models = sorted(table.values(), key=lambda m: (m.provider, m.input_per_1m))
251
+ rows = []
252
+ for mp in models:
253
+ if provider and mp.provider.lower() != provider.lower():
254
+ continue
255
+ rows.append({
256
+ "display_name": mp.display_name,
257
+ "provider": mp.provider,
258
+ "input_per_1m": mp.input_per_1m,
259
+ "output_per_1m": mp.output_per_1m,
260
+ "context": mp.max_context_tokens,
261
+ "tier": mp.family,
262
+ })
263
+ else:
264
+ rows = sorted(
265
+ [m for m in _FALLBACK_MODELS if not provider or m["provider"].lower() == provider.lower()],
266
+ key=lambda m: (m["provider"], m["input_per_1m"])
267
+ )
268
+ rows = [dict(display_name=m["display_name"], provider=m["provider"],
269
+ input_per_1m=m["input_per_1m"], output_per_1m=m["output_per_1m"],
270
+ context=m["context"], tier=m["tier"]) for m in rows]
271
+
272
+ if not rows:
273
+ return f"No models found for provider '{provider}'."
274
+
275
+ lines = [f"{'Model':<30} {'Provider':<12} {'Input/1M':>10} {'Output/1M':>11} {'Context':>10} {'Tier':<10}"]
276
+ lines.append("─" * 85)
277
+ for r in rows:
278
+ ctx = _fmt_tokens(r["context"])
279
+ lines.append(
280
+ f"{r['display_name']:<30} {r['provider']:<12} ${r['input_per_1m']:>8.2f} "
281
+ f"${r['output_per_1m']:>9.2f} {ctx:>10} {r['tier']:<10}"
282
+ )
283
+ lines.append(f"\n{len(rows)} models listed.")
284
+ return "\n".join(lines)
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # Tool 4: buildshield_optimize_prompt
289
+ # ---------------------------------------------------------------------------
290
+
291
+ _FILLER_REPLACEMENTS = [
292
+ (r"\bplease\s+", ""),
293
+ (r"\bkindly\s+", ""),
294
+ (r"\bI would like you to\s+", ""),
295
+ (r"\bmake sure to\s+", "ensure "),
296
+ (r"\bin order to\b", "to"),
297
+ (r"\bas well as\b", "and"),
298
+ (r"\bin the event that\b", "if"),
299
+ (r"\bat this point in time\b", "now"),
300
+ (r"\bdue to the fact that\b", "because"),
301
+ (r"\bfor the purpose of\b", "for"),
302
+ (r"\bit is important to note that\b", "Note:"),
303
+ (r"\bplease note that\b", "Note:"),
304
+ (r"\bit should be noted that\b", "Note:"),
305
+ (r"\bfeel free to\b", ""),
306
+ (r"\bdo not hesitate to\b", ""),
307
+ (r"\bI would appreciate it if you could\b", ""),
308
+ (r"\bwould you be able to\b", ""),
309
+ (r"\bcould you please\b", ""),
310
+ ]
311
+
312
+
313
+ def _word_overlap(a: str, b: str) -> float:
314
+ wa, wb = set(a.lower().split()), set(b.lower().split())
315
+ if not wa or not wb:
316
+ return 0.0
317
+ return len(wa & wb) / max(len(wa), len(wb))
318
+
319
+
320
+ def _dedup_paragraphs(text: str) -> tuple[str, int]:
321
+ paras = [p.strip() for p in re.split(r"\n{2,}", text) if p.strip()]
322
+ removed = 0
323
+ result = []
324
+ for i, para in enumerate(paras):
325
+ is_dup = False
326
+ for j in range(i + 1, len(paras)):
327
+ if _word_overlap(para, paras[j]) > 0.60:
328
+ is_dup = True
329
+ removed += 1
330
+ break
331
+ if not is_dup:
332
+ result.append(para)
333
+ return "\n\n".join(result), removed
334
+
335
+
336
+ def handle_optimize_prompt(text: str, aggressiveness: str = "medium") -> str:
337
+ """Rule-based prompt compression. No AI — works offline. Saves 20-40% on verbose prompts."""
338
+ if not text or not text.strip():
339
+ return "Empty prompt provided. Please pass the text you want to optimize."
340
+
341
+ original_tokens = max(1, len(text) // 4)
342
+ techniques: list[str] = []
343
+ optimized = text
344
+
345
+ # ---- Light: whitespace normalization ----
346
+ before = optimized
347
+ optimized = re.sub(r"[ \t]+", " ", optimized) # collapse spaces/tabs
348
+ optimized = re.sub(r"\n{3,}", "\n\n", optimized) # max 2 blank lines
349
+ optimized = "\n".join(line.rstrip() for line in optimized.splitlines())
350
+ optimized = optimized.strip()
351
+ if optimized != before:
352
+ techniques.append("Whitespace normalization")
353
+
354
+ if aggressiveness in ("medium", "heavy"):
355
+ # ---- Medium: filler phrase removal ----
356
+ before = optimized
357
+ for pattern, replacement in _FILLER_REPLACEMENTS:
358
+ optimized = re.sub(pattern, replacement, optimized, flags=re.IGNORECASE)
359
+ # Clean up double spaces left by removals
360
+ optimized = re.sub(r" +", " ", optimized)
361
+ optimized = re.sub(r"^ +", "", optimized, flags=re.MULTILINE)
362
+ if optimized != before:
363
+ techniques.append("Filler phrase removal")
364
+
365
+ if aggressiveness == "heavy":
366
+ # ---- Heavy: paragraph deduplication ----
367
+ before = optimized
368
+ optimized, removed_count = _dedup_paragraphs(optimized)
369
+ if removed_count:
370
+ techniques.append(f"Duplicate paragraph removal ({removed_count} removed)")
371
+
372
+ optimized_tokens = max(1, len(optimized) // 4)
373
+ savings_pct = round((1 - optimized_tokens / original_tokens) * 100, 1)
374
+
375
+ if not techniques:
376
+ return (
377
+ f"Prompt analyzed — no optimizations needed.\n"
378
+ f"Tokens: {original_tokens:,} | The prompt is already concise."
379
+ )
380
+
381
+ # Format output
382
+ lines = [
383
+ f"Optimization complete ({aggressiveness} mode)",
384
+ f"",
385
+ f"Original: {original_tokens:,} tokens",
386
+ f"Optimized: {optimized_tokens:,} tokens",
387
+ f"Saved: {original_tokens - optimized_tokens:,} tokens ({savings_pct}%)",
388
+ f"",
389
+ f"Techniques applied:",
390
+ ]
391
+ for t in techniques:
392
+ lines.append(f" • {t}")
393
+ lines += [
394
+ f"",
395
+ f"--- OPTIMIZED PROMPT ---",
396
+ optimized,
397
+ f"--- END ---",
398
+ ]
399
+ return "\n".join(lines)
400
+
401
+
402
+ # ---------------------------------------------------------------------------
403
+ # Tool 5: buildshield_check_budget
404
+ # ---------------------------------------------------------------------------
405
+
406
+ async def handle_check_budget(api_key: str | None = None, period: str = "today") -> str:
407
+ """Check your BuildShield budget. Requires a BuildShield API key (bsk_...)."""
408
+ if not api_key:
409
+ return (
410
+ "This tool requires a BuildShield API key.\n\n"
411
+ "Get your key at: https://thebuildshield.com/settings\n"
412
+ "Then pass it as: api_key='bsk_...'\n\n"
413
+ "Or configure it via the BUILDSHIELD_API_KEY environment variable."
414
+ )
415
+
416
+ if not api_key.startswith("bsk_"):
417
+ return f"Invalid key format. BuildShield keys start with 'bsk_'. Got: '{api_key[:8]}...'"
418
+
419
+ try:
420
+ import httpx
421
+ async with httpx.AsyncClient(timeout=10.0) as client:
422
+ r = await client.get(
423
+ f"{_API_BASE}/api/v1/agent/budget",
424
+ params={"period": period},
425
+ headers={"X-Api-Key": api_key},
426
+ )
427
+ if r.status_code == 401:
428
+ return "Invalid or expired API key. Check your key at https://thebuildshield.com/settings"
429
+ if r.status_code == 404:
430
+ return "No budget configured for this key. Set a budget at https://thebuildshield.com/settings"
431
+ r.raise_for_status()
432
+ data = r.json()
433
+ budget = data.get("budget", {})
434
+ quota = data.get("quota", {})
435
+ limit = budget.get("limit_usd") or 0
436
+ used = budget.get("used_usd") or 0
437
+ remaining = budget.get("remaining_usd")
438
+ if remaining is None:
439
+ remaining = limit - used
440
+ resets = budget.get("reset_at") or "not set"
441
+ calls_used = quota.get("calls_used_this_month", 0)
442
+ calls_limit = quota.get("calls_limit")
443
+ pct = round((used / limit * 100) if limit else 0, 1)
444
+ bar_filled = int(pct / 5)
445
+ bar = "█" * bar_filled + "░" * (20 - bar_filled)
446
+ plan = data.get("plan", "unknown")
447
+ quota_line = f"API calls: {calls_used}/{calls_limit if calls_limit else '∞'} this month"
448
+ if budget.get("has_budget"):
449
+ return (
450
+ f"Budget Status ({period}) — Plan: {plan}\n"
451
+ f"{'─'*30}\n"
452
+ f"Used: ${used:.4f} / ${limit:.2f}\n"
453
+ f"Remaining: ${remaining:.4f}\n"
454
+ f"Progress: [{bar}] {pct}%\n"
455
+ f"Resets at: {resets}\n"
456
+ f"{quota_line}"
457
+ )
458
+ else:
459
+ return (
460
+ f"Budget Status — Plan: {plan}\n"
461
+ f"{'─'*30}\n"
462
+ f"No spending budget configured.\n"
463
+ f"Set a budget at: https://thebuildshield.com/settings\n"
464
+ f"{quota_line}"
465
+ )
466
+ except ImportError:
467
+ return "httpx not installed. Run: pip install httpx"
468
+ except Exception as e:
469
+ return f"Could not reach BuildShield API: {e}\nCheck your connection or try again later."
470
+
471
+
472
+ # ---------------------------------------------------------------------------
473
+ # Tool 6: buildshield_log_usage
474
+ # ---------------------------------------------------------------------------
475
+
476
+ async def handle_log_usage(
477
+ model: str,
478
+ input_tokens: int,
479
+ output_tokens: int,
480
+ api_key: str | None = None,
481
+ task_description: str | None = None,
482
+ ) -> str:
483
+ """Log AI usage to BuildShield. Always calculates cost locally; syncs to cloud if api_key provided."""
484
+ # Calculate cost locally first (always works offline)
485
+ if _PRICING_AVAILABLE:
486
+ cost = calculate_cost(model, input_tokens, output_tokens)
487
+ mp = resolve_model(model)
488
+ model_display = mp.display_name if mp else model
489
+ else:
490
+ cost = _fallback_calculate(model, input_tokens, output_tokens)
491
+ m = _fallback_resolve(model)
492
+ model_display = m["display_name"] if m else model
493
+
494
+ if cost is None:
495
+ cost_str = "(model not found — cost unknown)"
496
+ cost = 0.0
497
+ else:
498
+ cost_str = f"${cost:.6f}"
499
+
500
+ local_summary = (
501
+ f"Usage logged locally\n"
502
+ f"Model: {model_display}\n"
503
+ f"Input: {input_tokens:,} tokens\n"
504
+ f"Output: {output_tokens:,} tokens\n"
505
+ f"Cost: {cost_str}"
506
+ )
507
+ if task_description:
508
+ local_summary += f"\nTask: {task_description}"
509
+
510
+ if not api_key:
511
+ return local_summary + "\n\nNote: Pass api_key='bsk_...' to sync to your BuildShield dashboard."
512
+
513
+ # Sync to API
514
+ try:
515
+ import httpx
516
+ payload = {
517
+ "model": model,
518
+ "input_tokens": input_tokens,
519
+ "output_tokens": output_tokens,
520
+ "estimated_cost_usd": cost,
521
+ "source": "mcp_server",
522
+ }
523
+ if task_description:
524
+ payload["task_description"] = task_description
525
+
526
+ async with httpx.AsyncClient(timeout=10.0) as client:
527
+ r = await client.post(
528
+ f"{_API_BASE}/api/v1/agent/usage",
529
+ json=payload,
530
+ headers={"X-Api-Key": api_key},
531
+ )
532
+ if r.status_code in (200, 201):
533
+ data = r.json()
534
+ remaining = data.get("budget_remaining_usd")
535
+ synced_note = "✓ Synced to BuildShield dashboard"
536
+ if remaining is not None:
537
+ synced_note += f" | Budget remaining: ${remaining:.4f}"
538
+ return local_summary + f"\n\n{synced_note}"
539
+ elif r.status_code == 429:
540
+ return local_summary + "\n\n⚠ Budget limit reached — usage logged locally only."
541
+ else:
542
+ return local_summary + f"\n\nCloud sync failed (HTTP {r.status_code}) — usage logged locally."
543
+ except ImportError:
544
+ return local_summary + "\n\nhttpx not installed. Run: pip install httpx"
545
+ except Exception as e:
546
+ return local_summary + f"\n\nCloud sync failed: {e} — usage logged locally."
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "buildshield-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for BuildShield — AI cost intelligence for agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["mcp", "ai", "cost", "llm", "buildshield"]
13
+ dependencies = [
14
+ "mcp>=1.0.0",
15
+ "httpx>=0.27.0",
16
+ ]
17
+
18
+ [project.scripts]
19
+ buildshield-mcp = "buildshield_mcp.server:main_sync"
20
+
21
+ [project.urls]
22
+ Homepage = "https://thebuildshield.com"
23
+ Repository = "https://github.com/Dev-31/theBuildShield.com"