mcpswitch-cli 0.1.0__py3-none-any.whl

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.
mcpswitch/usage.py ADDED
@@ -0,0 +1,232 @@
1
+ """Usage tracking — log every MCP tool call to SQLite.
2
+
3
+ This is the data moat. Every tool call logged here powers:
4
+ - Waste detection (Pro): "you loaded this MCP 200 sessions, called 0 tools"
5
+ - Predictive loading (Pro): "based on your history, you'll need github in 2 turns"
6
+ - Team analytics (Team): "your team wasted X tokens in MCP overhead this month"
7
+ """
8
+
9
+ import json
10
+ import sqlite3
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ USAGE_DB = Path.home() / ".mcpswitch" / "usage.db"
16
+ USAGE_DB.parent.mkdir(parents=True, exist_ok=True)
17
+
18
+
19
+ def _get_db() -> sqlite3.Connection:
20
+ conn = sqlite3.connect(str(USAGE_DB))
21
+ conn.execute("""
22
+ CREATE TABLE IF NOT EXISTS tool_calls (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ session_id TEXT,
25
+ server_name TEXT NOT NULL,
26
+ tool_name TEXT NOT NULL,
27
+ called_at REAL NOT NULL,
28
+ profile_name TEXT,
29
+ project_dir TEXT,
30
+ success INTEGER DEFAULT 1
31
+ )
32
+ """)
33
+ conn.execute("""
34
+ CREATE TABLE IF NOT EXISTS sessions (
35
+ session_id TEXT PRIMARY KEY,
36
+ profile_name TEXT,
37
+ project_dir TEXT,
38
+ started_at REAL NOT NULL,
39
+ ended_at REAL,
40
+ tools_called INTEGER DEFAULT 0,
41
+ tokens_saved INTEGER DEFAULT 0
42
+ )
43
+ """)
44
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_server ON tool_calls(server_name)")
45
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_called_at ON tool_calls(called_at)")
46
+ conn.commit()
47
+ return conn
48
+
49
+
50
+ def log_tool_call(
51
+ tool_name: str,
52
+ session_id: Optional[str] = None,
53
+ profile_name: Optional[str] = None,
54
+ project_dir: Optional[str] = None,
55
+ success: bool = True,
56
+ ) -> None:
57
+ """Log a single MCP tool call. Called from PostToolUse hook."""
58
+ # Extract server name from tool name (mcp__github__create_pr -> github)
59
+ server_name = _extract_server(tool_name)
60
+ if not server_name:
61
+ return # Not an MCP tool call
62
+
63
+ try:
64
+ conn = _get_db()
65
+ conn.execute(
66
+ """INSERT INTO tool_calls
67
+ (session_id, server_name, tool_name, called_at, profile_name, project_dir, success)
68
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
69
+ (session_id, server_name, tool_name, time.time(),
70
+ profile_name, project_dir, int(success))
71
+ )
72
+ conn.commit()
73
+ conn.close()
74
+ except Exception:
75
+ pass
76
+
77
+
78
+ def _extract_server(tool_name: str) -> Optional[str]:
79
+ """Extract server name from MCP tool name.
80
+
81
+ mcp__github__create_pull_request -> github
82
+ mcp__brave-search__search -> brave-search
83
+ github__create_pull_request -> github (some servers drop the mcp__ prefix)
84
+ """
85
+ if tool_name.startswith("mcp__"):
86
+ parts = tool_name.split("__")
87
+ return parts[1] if len(parts) >= 2 else None
88
+ # Some tools use server__tool format without mcp__ prefix
89
+ if "__" in tool_name:
90
+ return tool_name.split("__")[0]
91
+ return None
92
+
93
+
94
+ def get_server_usage_stats(days: int = 30) -> list[dict]:
95
+ """Return usage stats per server for the last N days.
96
+
97
+ Used for waste detection: servers loaded but rarely/never called.
98
+ """
99
+ since = time.time() - (days * 86400)
100
+ try:
101
+ conn = _get_db()
102
+ rows = conn.execute("""
103
+ SELECT
104
+ server_name,
105
+ COUNT(*) as total_calls,
106
+ COUNT(DISTINCT DATE(called_at, 'unixepoch')) as active_days,
107
+ COUNT(DISTINCT session_id) as sessions_used,
108
+ MAX(called_at) as last_used
109
+ FROM tool_calls
110
+ WHERE called_at > ?
111
+ GROUP BY server_name
112
+ ORDER BY total_calls DESC
113
+ """, (since,)).fetchall()
114
+ conn.close()
115
+ return [
116
+ {
117
+ "server": r[0],
118
+ "total_calls": r[1],
119
+ "active_days": r[2],
120
+ "sessions_used": r[3],
121
+ "last_used": r[4],
122
+ "last_used_days_ago": round((time.time() - r[4]) / 86400, 1) if r[4] else None,
123
+ }
124
+ for r in rows
125
+ ]
126
+ except Exception:
127
+ return []
128
+
129
+
130
+ def get_waste_report(loaded_servers: list[str], days: int = 30) -> list[dict]:
131
+ """Compare loaded servers vs actually called servers.
132
+
133
+ Returns list of servers that are loaded but rarely/never used.
134
+ This is the core Pro feature.
135
+ """
136
+ usage = {s["server"]: s for s in get_server_usage_stats(days)}
137
+ waste = []
138
+
139
+ for server in loaded_servers:
140
+ stats = usage.get(server)
141
+ if stats is None:
142
+ # Loaded but NEVER called in last N days
143
+ waste.append({
144
+ "server": server,
145
+ "total_calls": 0,
146
+ "sessions_used": 0,
147
+ "last_used_days_ago": None,
148
+ "waste_level": "high",
149
+ "recommendation": f"Never called in {days} days. Remove from profile.",
150
+ })
151
+ elif stats["total_calls"] < 3:
152
+ # Loaded but almost never called
153
+ waste.append({
154
+ "server": server,
155
+ "total_calls": stats["total_calls"],
156
+ "sessions_used": stats["sessions_used"],
157
+ "last_used_days_ago": stats["last_used_days_ago"],
158
+ "waste_level": "medium",
159
+ "recommendation": f"Only {stats['total_calls']} calls in {days} days. Consider removing.",
160
+ })
161
+
162
+ return sorted(waste, key=lambda x: x["total_calls"])
163
+
164
+
165
+ def get_session_tool_sequence(session_id: str) -> list[str]:
166
+ """Return ordered list of tools called in a session.
167
+
168
+ Used for predictive loading: if users always call github then playwright,
169
+ preload both when github profile is active.
170
+ """
171
+ try:
172
+ conn = _get_db()
173
+ rows = conn.execute(
174
+ "SELECT tool_name FROM tool_calls WHERE session_id = ? ORDER BY called_at",
175
+ (session_id,)
176
+ ).fetchall()
177
+ conn.close()
178
+ return [r[0] for r in rows]
179
+ except Exception:
180
+ return []
181
+
182
+
183
+ def get_top_tools_by_server(server_name: str, limit: int = 10) -> list[dict]:
184
+ """Return the most-called tools from a specific server.
185
+
186
+ Used to build minimal profiles: instead of loading ALL 30 github tools,
187
+ load only the 5 you actually use.
188
+ """
189
+ try:
190
+ conn = _get_db()
191
+ rows = conn.execute("""
192
+ SELECT tool_name, COUNT(*) as calls
193
+ FROM tool_calls
194
+ WHERE server_name = ?
195
+ GROUP BY tool_name
196
+ ORDER BY calls DESC
197
+ LIMIT ?
198
+ """, (server_name, limit)).fetchall()
199
+ conn.close()
200
+ return [{"tool": r[0], "calls": r[1]} for r in rows]
201
+ except Exception:
202
+ return []
203
+
204
+
205
+ def get_usage_summary(days: int = 7) -> dict:
206
+ """High-level summary for the weekly digest email."""
207
+ since = time.time() - (days * 86400)
208
+ try:
209
+ conn = _get_db()
210
+ total_calls = conn.execute(
211
+ "SELECT COUNT(*) FROM tool_calls WHERE called_at > ?", (since,)
212
+ ).fetchone()[0]
213
+
214
+ unique_servers = conn.execute(
215
+ "SELECT COUNT(DISTINCT server_name) FROM tool_calls WHERE called_at > ?", (since,)
216
+ ).fetchone()[0]
217
+
218
+ most_used = conn.execute("""
219
+ SELECT server_name, COUNT(*) as c FROM tool_calls
220
+ WHERE called_at > ?
221
+ GROUP BY server_name ORDER BY c DESC LIMIT 3
222
+ """, (since,)).fetchall()
223
+
224
+ conn.close()
225
+ return {
226
+ "days": days,
227
+ "total_calls": total_calls,
228
+ "unique_servers": unique_servers,
229
+ "top_servers": [{"server": r[0], "calls": r[1]} for r in most_used],
230
+ }
231
+ except Exception:
232
+ return {"days": days, "total_calls": 0, "unique_servers": 0, "top_servers": []}
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcpswitch-cli
3
+ Version: 0.1.0
4
+ Summary: Smart MCP profile manager for Claude Code — save 30-40% of your context window
5
+ License: MIT
6
+ Project-URL: Homepage, https://mcpswitch.dev
7
+ Project-URL: Repository, https://github.com/sathibabunaidu58/mcpswitch
8
+ Project-URL: Issues, https://github.com/sathibabunaidu58/mcpswitch/issues
9
+ Keywords: claude,mcp,claude-code,context-window,tokens,llm
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: click>=8.0
19
+ Requires-Dist: rich>=13.0
20
+ Requires-Dist: tiktoken>=0.5
21
+ Provides-Extra: server
22
+ Requires-Dist: flask>=3.0; extra == "server"
23
+ Requires-Dist: requests>=2.31; extra == "server"
24
+
25
+ # MCPSwitch
26
+
27
+ **Smart MCP profile manager for Claude Code.**
28
+ Stop wasting 30-40% of your context window on MCP tools you never use.
29
+
30
+ ---
31
+
32
+ ## The Problem
33
+
34
+ Every MCP server you install adds its tool schemas to Claude's context window at session start.
35
+ 84 tools across 6 servers = **15,540 tokens consumed before you type a single message.**
36
+ That's up to 40% of your context window gone — every session, every time.
37
+
38
+ MCPSwitch lets you define lean profiles for different workflows and switch between them instantly.
39
+
40
+ ---
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install mcpswitch
46
+ ```
47
+
48
+ Or run from source:
49
+ ```bash
50
+ git clone https://github.com/sathibabu/mcpswitch
51
+ cd mcpswitch
52
+ pip install -e .
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Quick Start
58
+
59
+ ```bash
60
+ # See what your current MCP config is costing you
61
+ mcpswitch analyze
62
+
63
+ # Save your current config as a profile
64
+ mcpswitch import --name full
65
+
66
+ # Create a lean profile
67
+ mcpswitch create python-backend
68
+
69
+ # Add only the servers you need for Python work
70
+ mcpswitch add python-backend github
71
+ mcpswitch add python-backend context7
72
+
73
+ # Switch to it before starting a Claude Code session
74
+ mcpswitch use python-backend
75
+
76
+ # See all profiles and their token costs
77
+ mcpswitch list
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Commands
83
+
84
+ | Command | What it does |
85
+ |---------|-------------|
86
+ | `mcpswitch status` | Show active profile + token cost of loaded servers |
87
+ | `mcpswitch analyze` | Break down token cost by server, show savings potential |
88
+ | `mcpswitch list` | List all profiles with token estimates |
89
+ | `mcpswitch use <profile>` | Switch to a profile (rewrites Claude config) |
90
+ | `mcpswitch import --name <n>` | Save current config as a named profile |
91
+ | `mcpswitch create <profile>` | Create a new empty profile |
92
+ | `mcpswitch add <profile> <server>` | Add a server to a profile |
93
+ | `mcpswitch remove <profile> <server>` | Remove a server from a profile |
94
+ | `mcpswitch save <profile>` | Save current active config as profile |
95
+ | `mcpswitch delete <profile>` | Delete a profile |
96
+
97
+ ---
98
+
99
+ ## Real-World Savings
100
+
101
+ | Scenario | All MCPs | Lean Profile | Saved |
102
+ |----------|----------|-------------|-------|
103
+ | Python backend work | 15,540 tokens | 3,200 tokens | 12,340 tokens (79%) |
104
+ | Frontend work | 15,540 tokens | 4,100 tokens | 11,440 tokens (74%) |
105
+ | Writing/docs | 15,540 tokens | 900 tokens | 14,640 tokens (94%) |
106
+
107
+ Fewer wasted tokens = more context for your actual code = better Claude responses.
108
+
109
+ ---
110
+
111
+ ## How It Works
112
+
113
+ MCPSwitch reads and writes the Claude MCP config files:
114
+ - **Claude Code CLI**: `~/.claude/claude_desktop_config.json`
115
+ - **Claude Desktop**: `~/AppData/Roaming/Claude/claude_desktop_config.json` (Windows)
116
+
117
+ Profiles are stored in `~/.mcpswitch/profiles.json`. Every config change is backed up automatically before overwriting.
118
+
119
+ ---
120
+
121
+ ## Pricing
122
+
123
+ - **Free**: unlimited profiles, all commands, open source
124
+ - **Pro** ($19/month): coming soon — auto-detect project type and switch profiles automatically, usage analytics, team profile sharing
125
+
126
+ ---
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,19 @@
1
+ mcpswitch/__init__.py,sha256=LCoxxRWg1T6LVsWLKmGa7W6XB4Vf7Sv1vhQh6z4Lqjk,86
2
+ mcpswitch/auto.py,sha256=Kh6i4KH-SxuyfMlh2D-lnCsNkw48f0sKFh76XU6FCLk,11574
3
+ mcpswitch/billing.py,sha256=Hy0m4JSuyuP2riS5c5hlatcE5Kieue4YvFwvUfeQahU,6569
4
+ mcpswitch/cli.py,sha256=oz9hoPDjprr2RAx2QuAAorw5NfjBBWnRzQa_HKz4xZU,49492
5
+ mcpswitch/community.py,sha256=2OG15XZrWX4HSomZRFxZKCF2qycTJ5xQ_mowl0BXrZg,7308
6
+ mcpswitch/config.py,sha256=twj03wgKbl_jVtbIOAs1czkqWwgYTjfvG3uSgP-Ixag,2004
7
+ mcpswitch/email.py,sha256=0V3CJ-fNqtxW-CGNUIp8V78HBHi5hAci_VbSUnc8GsA,7635
8
+ mcpswitch/hooks.py,sha256=VmUCZy4tUqcsJKIMY61PS30fCCao-VeoiRTRj_EKZB8,7556
9
+ mcpswitch/profiles.py,sha256=YkD-kutmYHonePLvtoqa3QkVduEj26cK0UuUkypTG2I,2786
10
+ mcpswitch/sync.py,sha256=4C6CXrb0EMKoKH3F_47qGpKSLLD7CI5pcQj8DbsNpus,7146
11
+ mcpswitch/team.py,sha256=_RCdj5KM_M3H5G_PqjbYGNetnUYx_754wq-B1KWpPOQ,21030
12
+ mcpswitch/tier.py,sha256=gDNFP7mVAdc-hqjy9xg_6kNbajcE_FlK21fSL3Hvk-c,6058
13
+ mcpswitch/tokens.py,sha256=GHyUIBRGGgiQ9176j6XH_kGHxcWhqj9hCye5ziHkLiI,12780
14
+ mcpswitch/usage.py,sha256=AstK9F9psGJkh9En1k-mI1PEQHHxbUEiXyFfHOM3Qwk,7865
15
+ mcpswitch_cli-0.1.0.dist-info/METADATA,sha256=ZXYGW56ooIRfS6Xw305V6dS_jlTwadH0a02gJkzTIEA,4049
16
+ mcpswitch_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ mcpswitch_cli-0.1.0.dist-info/entry_points.txt,sha256=lZXhszGsVir83rv4DfpiP2x__wv8F2kUh-oiEvTSOJQ,49
18
+ mcpswitch_cli-0.1.0.dist-info/top_level.txt,sha256=3FTIQDqq6o1wZysYwsiKzM47ZXAApKdzfV2SyJt_cHc,10
19
+ mcpswitch_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcpswitch = mcpswitch.cli:main
@@ -0,0 +1 @@
1
+ mcpswitch