mcpskills-cli 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,104 @@
1
+ ---
2
+ name: dellstore
3
+ description: MCP tools for dellstore. Available tools: list_tables, get_table_schema, execute_query, test_connection. Use when the user needs to call these MCP tools.
4
+ ---
5
+
6
+ # dellstore MCP Tools
7
+
8
+
9
+
10
+ ## Tables
11
+
12
+ **categories** — category (int PK), categoryname (varchar 50)
13
+
14
+ **cust_hist** — customerid (int, FK→customers.customerid), orderid (int), prod_id (int)
15
+
16
+ **customers** — customerid (int PK), firstname, lastname, address1, address2 (nullable), city, state (nullable), zip (int, nullable), country, region (smallint), email (nullable), phone (nullable), creditcardtype (int), creditcard, creditcardexpiration, username (unique), password, age (smallint, nullable), income (int, nullable), gender (varchar 1, nullable) — all strings varchar 50 unless noted
17
+
18
+ **inventory** — prod_id (int PK), quan_in_stock (int), sales (int)
19
+
20
+ **orderlines** — orderlineid (int), orderid (int, FK→orders.orderid), prod_id (int), quantity (smallint), orderdate (date)
21
+
22
+ **orders** — orderid (int PK), orderdate (date), customerid (int, nullable, FK→customers.customerid), netamount (numeric), tax (numeric), totalamount (numeric)
23
+
24
+ **products** — prod_id (int PK), category (int), title (varchar 50), actor (varchar 50), price (numeric), special (smallint, nullable), common_prod_id (int)
25
+
26
+ **reorder** — prod_id (int), date_low (date), quan_low (int), date_reordered (date, nullable), quan_reordered (int, nullable), date_expected (date, nullable)
27
+
28
+ ## Available Tools
29
+
30
+ ### list_tables
31
+
32
+ List all tables in the database.
33
+
34
+ Args:
35
+ db_name: Identifier for the database instance (default: "default")
36
+
37
+ **Execute:**
38
+ ```python
39
+ python3 /Users/dhanababu/workspace/forge-mcp-to-skills/.cursor/skills/dellstore/scripts/call.py list_tables
40
+ ```
41
+
42
+ ### get_table_schema
43
+
44
+ Get schema information for a specific table.
45
+
46
+ Args:
47
+ table_name: Name of the table to inspect
48
+ db_name: Identifier for the database instance (default: "default")
49
+
50
+ Returns:
51
+ JSON string with table schema information
52
+
53
+ **Parameters:**
54
+ - `table_name` (string, required): -
55
+
56
+ **Execute:**
57
+ ```python
58
+ python3 /Users/dhanababu/workspace/forge-mcp-to-skills/.cursor/skills/dellstore/scripts/call.py get_table_schema '{"table_name": "<table_name>"}'
59
+ ```
60
+
61
+ ### execute_query
62
+
63
+ Execute a SQL query with optional parameters.
64
+
65
+ This tool allows you to execute any SQL query
66
+ (SELECT, INSERT, UPDATE, DELETE, etc.)
67
+ with support for parameterized queries to prevent SQL injection.
68
+
69
+ Args:
70
+ query: SQL query to execute
71
+ params: Optional dictionary of parameters for parameterized queries
72
+ db_name: Identifier for the database instance (default: "default")
73
+
74
+ Returns:
75
+ For SELECT queries: List of dictionaries representing rows
76
+ For INSERT/UPDATE/DELETE: List with affected_rows count
77
+
78
+ **Parameters:**
79
+ - `query` (string, required): -
80
+ - `params` (any, optional): -
81
+
82
+ **Execute:**
83
+ ```python
84
+ python3 /Users/dhanababu/workspace/forge-mcp-to-skills/.cursor/skills/dellstore/scripts/call.py execute_query '{"query": "<query>"}'
85
+ ```
86
+
87
+ ### test_connection
88
+
89
+ Test the database connection and return connection status information.
90
+
91
+ This tool verifies that the database connection is active and returns
92
+ connection details including database type, version, and pool
93
+ configuration.
94
+
95
+ Args:
96
+
97
+ Returns:
98
+ Dictionary with connection status, database information, and pool
99
+ settings
100
+
101
+ **Execute:**
102
+ ```python
103
+ python3 /Users/dhanababu/workspace/forge-mcp-to-skills/.cursor/skills/dellstore/scripts/call.py test_connection
104
+ ```
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """Call MCP tool on server 'dellstore'."""
3
+ import configparser
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.request
8
+
9
+ SERVER = "dellstore"
10
+ CREDS_FILE = os.path.expanduser("~/.mcps/credentials")
11
+
12
+
13
+ def load_credentials():
14
+ cfg = configparser.ConfigParser()
15
+ cfg.read(CREDS_FILE)
16
+ if SERVER not in cfg:
17
+ print(f"No section [{SERVER}] in {CREDS_FILE}", file=sys.stderr)
18
+ sys.exit(1)
19
+ return cfg[SERVER]["url"], cfg[SERVER]["token"]
20
+
21
+
22
+ def call_tool(tool_name: str, arguments: dict) -> dict:
23
+ url, token = load_credentials()
24
+ url = url.rstrip("/") + "/"
25
+ body = json.dumps({
26
+ "jsonrpc": "2.0",
27
+ "method": "tools/call",
28
+ "params": {"name": tool_name, "arguments": arguments},
29
+ "id": 1,
30
+ }).encode()
31
+ req = urllib.request.Request(
32
+ url, data=body, method="POST",
33
+ headers={
34
+ "Authorization": f"Bearer {token}",
35
+ "Content-Type": "application/json",
36
+ "Accept": "application/json, text/event-stream",
37
+ "MCP-Protocol-Version": "2025-06-18",
38
+ },
39
+ )
40
+ with urllib.request.urlopen(req, timeout=30) as resp:
41
+ text = resp.read().decode()
42
+ for line in text.strip().split("\n"):
43
+ if line.startswith("data:"):
44
+ line = line[5:].strip()
45
+ if not line or line == "[DONE]":
46
+ continue
47
+ try:
48
+ obj = json.loads(line)
49
+ if "result" in obj or "error" in obj:
50
+ return obj
51
+ except json.JSONDecodeError:
52
+ pass
53
+ return json.loads(text)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ if len(sys.argv) < 2:
58
+ print(f"Usage: {sys.argv[0]} <tool_name> [json_args]", file=sys.stderr)
59
+ sys.exit(1)
60
+ tool = sys.argv[1]
61
+ args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
62
+ result = call_tool(tool, args)
63
+ print(json.dumps(result, indent=2))
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcpskills-cli
3
+ Version: 0.1.0
4
+ Summary: Generate Cursor Agent Skills from MCP server tools
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: fastmcp>=2.3
7
+ Requires-Dist: jinja2>=3.1
@@ -0,0 +1,85 @@
1
+ # mcpskills-cli
2
+
3
+ Generate Agent Skills from MCP server tools. Connects via Streamable HTTP, discovers tools, and outputs a skill with schema docs and a call script in the language of your choice.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install .
9
+ # or for development:
10
+ pip install -e .
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ mcpskills-cli --url <MCP_SERVER_URL> --token <TOKEN> [--name <NAME>] [--output <DIR>] [--script <LANG>] [--multi-skills]
17
+ ```
18
+
19
+ | Flag | Default | Description |
20
+ |---|---|---|
21
+ | `--url` | (required) | MCP server endpoint |
22
+ | `--token` | (required) | Bearer token |
23
+ | `--name` | from URL | Server name (skill dir + credentials key) |
24
+ | `--output` | `~/.cursor/skills` | Skills output directory |
25
+ | `--script` | `bash` | Call script language: `bash`, `python`, `node`, `go`, `rust` |
26
+ | `--multi-skills` | `false` | Generate a separate skill for each tool |
27
+
28
+ ### Examples
29
+
30
+ ```bash
31
+ # Generate single skill with all tools (default)
32
+ mcpskills-cli --url http://localhost:8027/mcp/abc123 --token mytoken --name my-db
33
+
34
+ # Generate separate skill for each tool
35
+ mcpskills-cli --url http://localhost:8027/mcp/abc123 --token mytoken --name my-db --multi-skills
36
+
37
+ # Generate with Python call script
38
+ mcpskills-cli --url http://localhost:8027/mcp/abc123 --token mytoken --name my-db --script python
39
+
40
+ # Generate with Node.js call script
41
+ mcpskills-cli --url http://localhost:8027/mcp/abc123 --token mytoken --name my-db --script node
42
+ ```
43
+
44
+ ## Credentials
45
+
46
+ Stored in `~/.mcps/credentials` (INI format, `chmod 600`). One section per server, updated automatically.
47
+
48
+ ```ini
49
+ [my-db]
50
+ url = http://localhost:8027/mcp/abc123/
51
+ token = mytoken
52
+ ```
53
+
54
+ Rotate tokens by editing the file directly; no need to regenerate skills.
55
+
56
+ ## Generated Output
57
+
58
+ ### Default Mode (Single Skill)
59
+
60
+ ```
61
+ ~/.cursor/skills/<server-name>/
62
+ SKILL.md # Documents all tools with parameters
63
+ scripts/
64
+ call.<ext> # Calls any tool: ./call.<ext> <tool_name> '{"key":"val"}'
65
+ ```
66
+
67
+ ### Multi-Skills Mode (--multi-skills)
68
+
69
+ ```
70
+ ~/.cursor/skills/<server-name>-<tool-name-1>/
71
+ SKILL.md # Documents single tool
72
+ scripts/
73
+ call.<ext> # Calls tool: ./call.<ext> <tool_name> '{"key":"val"}'
74
+
75
+ ~/.cursor/skills/<server-name>-<tool-name-2>/
76
+ SKILL.md
77
+ scripts/
78
+ call.<ext>
79
+ ```
80
+
81
+ ## Requirements
82
+
83
+ - Python >= 3.10
84
+ - `fastmcp` >= 2.3
85
+ - `jinja2` >= 3.1
@@ -0,0 +1 @@
1
+ mcp-cli --url http://localhost:8032/mcp/8bb059d5-0115-4c08-b994-25c26695b027 --token mcp_token_Na7kQzvA2RxhzvAjT9P3Q0rPiaxlIHUnXiPHCoeMrhE --name neo4j-movie --script python --output .cursor/skills
@@ -0,0 +1,49 @@
1
+ # Auto-Generated Skills for Low Token Consumption
2
+
3
+ For questions like "show me top 10 products", token cost comes from:
4
+
5
+ 1. **Skill doc reads** – agent loads multiple SKILL.md files
6
+ 2. **Exploratory tool calls** – list_tables → get_table_schema → execute_query (3 round-trips)
7
+
8
+ ## Recommendations
9
+
10
+ ### 1. Generate a single skill (do not use `--multi-skills`)
11
+
12
+ - **One** SKILL.md that documents all MCP tools (list_tables, get_table_schema, execute_query, test_connection).
13
+ - Agent loads 1 skill instead of N → fewer tokens.
14
+ - Run: `mcpskills-cli --url <URL> --token <TOKEN> --name dellstore` (omit `--multi-skills`).
15
+
16
+ ### 2. Add usage guidance in the single skill
17
+
18
+ Include a short "When to use" so the agent skips discovery when not needed:
19
+
20
+ - **Known table / clear intent** (e.g. "top 10 products") → use **execute_query** only with a `SELECT ... LIMIT` query.
21
+ - **Unknown database** → use list_tables, then get_table_schema, then execute_query.
22
+
23
+ This reduces 3 tool calls to 1 for the common case.
24
+
25
+ ### 3. Optional: embed schema at generation time
26
+
27
+ If the generator can call the MCP at build time (list_tables + get_table_schema) and embed a "Known tables" section into SKILL.md, the agent gets table names and columns in one read and can go straight to execute_query. Requires a new option (e.g. `--include-schema`) and one-off MCP calls during `mcpskills-cli` run.
28
+
29
+ ### 4. MCP server: high-level tools (lowest tokens)
30
+
31
+ Expose tools such as:
32
+
33
+ - `get_products(limit=10, offset=0, category=...)`
34
+ or
35
+ - `query_table(table_name, limit, columns, order_by)`
36
+
37
+ Then the agent: 1 skill + 1 call. mcpskills-cli just generates the skill for that tool; no generator change.
38
+
39
+ ## Summary
40
+
41
+ | Approach | Skill reads | Tool calls (e.g. top 10 products) | Token impact |
42
+ |-----------------------|------------|------------------------------------|----------------|
43
+ | Multi-skills (current)| 3 | 3 (list + schema + query) | Highest |
44
+ | Single skill | 1 | 3 | Lower (docs) |
45
+ | Single skill + guidance | 1 | 1 (execute_query only) | Lower (docs + calls) |
46
+ | Single skill + embedded schema | 1 | 1 | Same as above |
47
+ | MCP high-level tool | 1 | 1 | Lowest |
48
+
49
+ **Practical minimum for mcpskills-cli:** generate **one skill** (no `--multi-skills`) and add **usage guidance** so the agent uses execute_query directly when the intent is clear.
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcpskills-cli"
7
+ version = "0.1.0"
8
+ description = "Generate Cursor Agent Skills from MCP server tools"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "fastmcp>=2.3",
12
+ "jinja2>=3.1",
13
+ ]
14
+
15
+ [project.scripts]
16
+ mcpskills-cli = "mcp_cli.cli:main"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/mcp_cli"]
@@ -0,0 +1,3 @@
1
+ """mcpskills-cli: Generate Cursor Agent Skills from MCP server tools."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,86 @@
1
+ """CLI entry point for mcpskills-cli."""
2
+
3
+ import argparse
4
+ import os
5
+ import re
6
+ import sys
7
+
8
+ from mcp_cli import credentials, client, generator
9
+
10
+ DEFAULT_OUTPUT = os.path.expanduser("~/.cursor/skills")
11
+ SCRIPT_CHOICES = list(generator.SCRIPT_LANG_MAP.keys())
12
+
13
+
14
+ def sanitize_name(s: str) -> str:
15
+ return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") or "mcp-server"
16
+
17
+
18
+ def derive_server_name(url: str) -> str:
19
+ u = url.rstrip("/")
20
+ if "/" in u:
21
+ return sanitize_name(u.split("/")[-1])
22
+ return sanitize_name(u.replace(":", "-"))
23
+
24
+
25
+ def main():
26
+ ap = argparse.ArgumentParser(
27
+ prog="mcpskills-cli",
28
+ description="Generate Cursor Agent Skill from MCP server tools",
29
+ )
30
+ ap.add_argument("--url", required=True, help="MCP server URL (Streamable HTTP)")
31
+ ap.add_argument("--token", required=True, help="Bearer token for auth")
32
+ ap.add_argument("--output", default=DEFAULT_OUTPUT, help=f"Skills output dir (default: {DEFAULT_OUTPUT})")
33
+ ap.add_argument("--name", help="Server name (default: derived from URL)")
34
+ ap.add_argument(
35
+ "--script",
36
+ choices=SCRIPT_CHOICES,
37
+ default="bash",
38
+ help="Language for the generated call script (default: bash)",
39
+ )
40
+ ap.add_argument(
41
+ "--multi-skills",
42
+ action="store_true",
43
+ help="Generate a separate skill for each tool",
44
+ )
45
+ args = ap.parse_args()
46
+
47
+ server_name = sanitize_name((args.name or derive_server_name(args.url)).strip())
48
+ url = args.url.rstrip("/") + "/"
49
+
50
+ try:
51
+ tools = client.list_tools(url, args.token)
52
+ except Exception as e:
53
+ print(f"Error fetching tools: {e}", file=sys.stderr)
54
+ sys.exit(1)
55
+
56
+ if not tools:
57
+ print("No tools returned by server.", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+ credentials.save(server_name, url, args.token)
61
+ print(f"Credentials saved to {credentials.CREDS_FILE} (chmod 600)")
62
+
63
+ result = generator.generate_skill(
64
+ server_name=server_name,
65
+ raw_tools=tools,
66
+ output_dir=os.path.expanduser(args.output),
67
+ script=args.script,
68
+ multi_skills=args.multi_skills,
69
+ )
70
+ lang_cfg = generator.SCRIPT_LANG_MAP[args.script]
71
+
72
+ if args.multi_skills:
73
+ print(f"\nSkills generated:")
74
+ for out_dir in result:
75
+ print(f" {out_dir}")
76
+ else:
77
+ out_dir = result
78
+ call_file = os.path.join(out_dir, "scripts", f"call.{lang_cfg['ext']}")
79
+ print(f"Skill generated at {out_dir}")
80
+ print(f" SKILL.md ({len(tools)} tools)")
81
+ print(f" scripts/call.{lang_cfg['ext']}")
82
+ print(f"Usage: {lang_cfg['bin']} {call_file} <tool_name> '{{}}' ")
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
@@ -0,0 +1,18 @@
1
+ """MCP client wrapper using fastmcp."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from fastmcp import Client
7
+ from fastmcp.client.transports import StreamableHttpTransport
8
+
9
+
10
+ async def _list_tools(url: str, token: str) -> list[dict[str, Any]]:
11
+ transport = StreamableHttpTransport(url, auth=token)
12
+ async with Client(transport) as client:
13
+ tools = await client.list_tools()
14
+ return [t.model_dump() for t in tools]
15
+
16
+
17
+ def list_tools(url: str, token: str) -> list[dict[str, Any]]:
18
+ return asyncio.run(_list_tools(url, token))
@@ -0,0 +1,41 @@
1
+ """Manage MCP server credentials in ~/.mcps/credentials (INI format, chmod 600)."""
2
+
3
+ import configparser
4
+ import os
5
+ from dataclasses import dataclass
6
+
7
+ CREDS_DIR = os.path.expanduser("~/.mcps")
8
+ CREDS_FILE = os.path.join(CREDS_DIR, "credentials")
9
+
10
+
11
+ @dataclass
12
+ class ServerCredentials:
13
+ url: str
14
+ token: str
15
+
16
+
17
+ def save(server_name: str, url: str, token: str) -> None:
18
+ os.makedirs(CREDS_DIR, mode=0o700, exist_ok=True)
19
+ cfg = configparser.ConfigParser()
20
+ if os.path.isfile(CREDS_FILE):
21
+ cfg.read(CREDS_FILE)
22
+ if server_name not in cfg:
23
+ cfg[server_name] = {}
24
+ cfg[server_name]["url"] = url
25
+ cfg[server_name]["token"] = token
26
+ with open(CREDS_FILE, "w") as f:
27
+ cfg.write(f)
28
+ os.chmod(CREDS_FILE, 0o600)
29
+
30
+
31
+ def load(server_name: str) -> ServerCredentials:
32
+ cfg = configparser.ConfigParser()
33
+ if not os.path.isfile(CREDS_FILE):
34
+ raise FileNotFoundError(f"Missing {CREDS_FILE}")
35
+ cfg.read(CREDS_FILE)
36
+ if server_name not in cfg:
37
+ raise KeyError(f"No section [{server_name}] in {CREDS_FILE}")
38
+ return ServerCredentials(
39
+ url=cfg[server_name]["url"],
40
+ token=cfg[server_name]["token"],
41
+ )
@@ -0,0 +1,170 @@
1
+ """Fetch MCP tools, render templates, write skill output."""
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import jinja2
9
+
10
+ SCRIPT_LANG_MAP = {
11
+ "bash": {
12
+ "template": "call_bash.sh.j2",
13
+ "ext": "sh",
14
+ "bin": "bash",
15
+ "lang": "bash",
16
+ },
17
+ "python": {
18
+ "template": "call_python.py.j2",
19
+ "ext": "py",
20
+ "bin": "python3",
21
+ "lang": "python",
22
+ },
23
+ "node": {
24
+ "template": "call_node.js.j2",
25
+ "ext": "js",
26
+ "bin": "node",
27
+ "lang": "javascript",
28
+ },
29
+ "go": {
30
+ "template": "call_go.go.j2",
31
+ "ext": "go",
32
+ "bin": "go run",
33
+ "lang": "go",
34
+ },
35
+ "rust": {
36
+ "template": "call_rust.rs.j2",
37
+ "ext": "rs",
38
+ "bin": "cargo run --",
39
+ "lang": "rust",
40
+ },
41
+ }
42
+
43
+
44
+ @dataclass
45
+ class ToolParam:
46
+ name: str
47
+ type: str
48
+ required: str
49
+ description: str
50
+
51
+
52
+ @dataclass
53
+ class ToolInfo:
54
+ name: str
55
+ description: str
56
+ params: list[ToolParam]
57
+ required_params: list[str]
58
+ example_args: str
59
+
60
+
61
+ def parse_tool(raw: dict[str, Any]) -> ToolInfo:
62
+ schema = raw.get("inputSchema") or {}
63
+ props = schema.get("properties") or {}
64
+ required_set = set(schema.get("required") or [])
65
+ params = []
66
+ for key, spec in props.items():
67
+ if isinstance(spec, dict):
68
+ params.append(ToolParam(
69
+ name=key,
70
+ type=spec.get("type", "any"),
71
+ required="required" if key in required_set else "optional",
72
+ description=(spec.get("description") or "").strip(),
73
+ ))
74
+ else:
75
+ params.append(
76
+ ToolParam(name=key, type="any", required="optional", description="")
77
+ )
78
+ req_params = [p.name for p in params if p.required == "required"]
79
+ example = (
80
+ json.dumps({k: f"<{k}>" for k in req_params}) if req_params else ""
81
+ )
82
+ return ToolInfo(
83
+ name=raw.get("name", "unknown"),
84
+ description=(raw.get("description") or "No description.").strip(),
85
+ params=params,
86
+ required_params=req_params,
87
+ example_args=example,
88
+ )
89
+
90
+
91
+ def _get_env() -> jinja2.Environment:
92
+ return jinja2.Environment(
93
+ loader=jinja2.PackageLoader("mcp_cli", "templates"),
94
+ keep_trailing_newline=True,
95
+ trim_blocks=True,
96
+ lstrip_blocks=True,
97
+ )
98
+
99
+
100
+ def generate_skill(
101
+ server_name: str,
102
+ raw_tools: list[dict[str, Any]],
103
+ output_dir: str,
104
+ script: str = "bash",
105
+ multi_skills: bool = False,
106
+ ) -> str | list[str]:
107
+ env = _get_env()
108
+ tools = [parse_tool(t) for t in raw_tools]
109
+ lang_cfg = SCRIPT_LANG_MAP[script]
110
+
111
+ if multi_skills:
112
+ generated_dirs = []
113
+ for tool in tools:
114
+ out_dir = os.path.join(output_dir, f"{server_name}-{tool.name}")
115
+ scripts_dir = os.path.join(out_dir, "scripts")
116
+ os.makedirs(scripts_dir, exist_ok=True)
117
+ skill_path = os.path.abspath(out_dir)
118
+
119
+ skill_md = env.get_template("skill_single.md.j2").render(
120
+ server_name=server_name,
121
+ tool=tool,
122
+ skill_path=skill_path,
123
+ script_lang=lang_cfg["lang"],
124
+ script_bin=lang_cfg["bin"],
125
+ script_ext=lang_cfg["ext"],
126
+ )
127
+ with open(os.path.join(out_dir, "SKILL.md"), "w") as f:
128
+ f.write(skill_md)
129
+
130
+ call_content = env.get_template(lang_cfg["template"]).render(
131
+ server_name=server_name
132
+ )
133
+ call_filename = f"call.{lang_cfg['ext']}"
134
+ call_path = os.path.join(scripts_dir, call_filename)
135
+ with open(call_path, "w") as f:
136
+ f.write(call_content)
137
+ os.chmod(call_path, 0o755)
138
+
139
+ generated_dirs.append(out_dir)
140
+
141
+ return generated_dirs
142
+ else:
143
+ tool_names = ", ".join(t.name for t in tools)
144
+ out_dir = os.path.join(output_dir, server_name)
145
+ scripts_dir = os.path.join(out_dir, "scripts")
146
+ os.makedirs(scripts_dir, exist_ok=True)
147
+ skill_path = os.path.abspath(out_dir)
148
+
149
+ skill_md = env.get_template("skill.md.j2").render(
150
+ server_name=server_name,
151
+ tools=tools,
152
+ tool_names=tool_names,
153
+ skill_path=skill_path,
154
+ script_lang=lang_cfg["lang"],
155
+ script_bin=lang_cfg["bin"],
156
+ script_ext=lang_cfg["ext"],
157
+ )
158
+ with open(os.path.join(out_dir, "SKILL.md"), "w") as f:
159
+ f.write(skill_md)
160
+
161
+ call_content = env.get_template(lang_cfg["template"]).render(
162
+ server_name=server_name
163
+ )
164
+ call_filename = f"call.{lang_cfg['ext']}"
165
+ call_path = os.path.join(scripts_dir, call_filename)
166
+ with open(call_path, "w") as f:
167
+ f.write(call_content)
168
+ os.chmod(call_path, 0o755)
169
+
170
+ return out_dir
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ SERVER="{{ server_name }}"
4
+ CREDS="${HOME}/.mcps/credentials"
5
+ if [[ ! -f "$CREDS" ]]; then
6
+ echo "Missing $CREDS" >&2
7
+ exit 1
8
+ fi
9
+ get_ini() {
10
+ local section="$1" key="$2"
11
+ awk -v S="\\[$section\\]" -v K="$key" '
12
+ $0 ~ S { found=1; next }
13
+ found && /^\[/ { exit }
14
+ found && $0 ~ "^" K "[[:space:]]*=" { sub(/^[^=]*=[[:space:]]*/, ""); print; exit }
15
+ ' "$CREDS"
16
+ }
17
+ URL=$(get_ini "$SERVER" "url")
18
+ TOKEN=$(get_ini "$SERVER" "token")
19
+ if [[ -z "$URL" || -z "$TOKEN" ]]; then
20
+ echo "Missing url or token for [$SERVER] in $CREDS" >&2
21
+ exit 1
22
+ fi
23
+ [[ "$URL" != */ ]] && URL="$URL/"
24
+ TOOL="$1"
25
+ ARGS="${2:-{}}"
26
+ if [[ -z "$TOOL" ]]; then
27
+ echo "Usage: $0 <tool_name> [json_args]" >&2
28
+ exit 1
29
+ fi
30
+ BODY=$(python3 -c "
31
+ import json, sys
32
+ name, raw = sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else '{}'
33
+ try: args = json.loads(raw)
34
+ except: args = {}
35
+ print(json.dumps({'jsonrpc':'2.0','method':'tools/call','params':{'name':name,'arguments':args},'id':1}))
36
+ " "$TOOL" "$ARGS")
37
+ RESPONSE=$(curl -s --max-time 30 -X POST "$URL" \
38
+ -H "Authorization: Bearer $TOKEN" \
39
+ -H "Content-Type: application/json" \
40
+ -H "Accept: application/json, text/event-stream" \
41
+ -H "MCP-Protocol-Version: 2025-06-18" \
42
+ -d "$BODY")
43
+ echo "$RESPONSE" | python3 -c "
44
+ import json, sys
45
+ text = sys.stdin.read().strip()
46
+ for line in text.split('\n'):
47
+ if line.startswith('data:'):
48
+ line = line[5:].strip()
49
+ if not line or line == '[DONE]': continue
50
+ try:
51
+ obj = json.loads(line)
52
+ if 'result' in obj or 'error' in obj:
53
+ print(json.dumps(obj, indent=2))
54
+ sys.exit(0)
55
+ except json.JSONDecodeError: pass
56
+ print(text)
57
+ "
@@ -0,0 +1,113 @@
1
+ // Call MCP tool on server '{{ server_name }}'.
2
+ // Usage: go run call.go <tool_name> [json_args]
3
+ package main
4
+
5
+ import (
6
+ "bufio"
7
+ "bytes"
8
+ "encoding/json"
9
+ "fmt"
10
+ "io"
11
+ "net/http"
12
+ "os"
13
+ "path/filepath"
14
+ "strings"
15
+ "time"
16
+ )
17
+
18
+ const server = "{{ server_name }}"
19
+
20
+ func loadCredentials() (string, string) {
21
+ home, _ := os.UserHomeDir()
22
+ data, err := os.ReadFile(filepath.Join(home, ".mcps", "credentials"))
23
+ if err != nil {
24
+ fmt.Fprintln(os.Stderr, err)
25
+ os.Exit(1)
26
+ }
27
+ var url, token string
28
+ inSection := false
29
+ scanner := bufio.NewScanner(bytes.NewReader(data))
30
+ for scanner.Scan() {
31
+ line := strings.TrimSpace(scanner.Text())
32
+ if line == fmt.Sprintf("[%s]", server) {
33
+ inSection = true
34
+ continue
35
+ }
36
+ if inSection && strings.HasPrefix(line, "[") {
37
+ break
38
+ }
39
+ if inSection {
40
+ if k, v, ok := strings.Cut(line, "="); ok {
41
+ switch strings.TrimSpace(k) {
42
+ case "url":
43
+ url = strings.TrimSpace(v)
44
+ case "token":
45
+ token = strings.TrimSpace(v)
46
+ }
47
+ }
48
+ }
49
+ }
50
+ if url == "" || token == "" {
51
+ fmt.Fprintf(os.Stderr, "Missing url or token for [%s]\n", server)
52
+ os.Exit(1)
53
+ }
54
+ if !strings.HasSuffix(url, "/") {
55
+ url += "/"
56
+ }
57
+ return url, token
58
+ }
59
+
60
+ func main() {
61
+ if len(os.Args) < 2 {
62
+ fmt.Fprintf(os.Stderr, "Usage: %s <tool_name> [json_args]\n", os.Args[0])
63
+ os.Exit(1)
64
+ }
65
+ toolName := os.Args[1]
66
+ args := json.RawMessage("{}")
67
+ if len(os.Args) > 2 {
68
+ args = json.RawMessage(os.Args[2])
69
+ }
70
+ url, token := loadCredentials()
71
+ body, _ := json.Marshal(map[string]any{
72
+ "jsonrpc": "2.0",
73
+ "method": "tools/call",
74
+ "params": map[string]any{"name": toolName, "arguments": args},
75
+ "id": 1,
76
+ })
77
+ req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
78
+ req.Header.Set("Authorization", "Bearer "+token)
79
+ req.Header.Set("Content-Type", "application/json")
80
+ req.Header.Set("Accept", "application/json, text/event-stream")
81
+ req.Header.Set("MCP-Protocol-Version", "2025-06-18")
82
+ client := &http.Client{Timeout: 30 * time.Second}
83
+ resp, err := client.Do(req)
84
+ if err != nil {
85
+ fmt.Fprintln(os.Stderr, err)
86
+ os.Exit(1)
87
+ }
88
+ defer resp.Body.Close()
89
+ raw, _ := io.ReadAll(resp.Body)
90
+ for _, line := range strings.Split(string(raw), "\n") {
91
+ line = strings.TrimSpace(line)
92
+ if strings.HasPrefix(line, "data:") {
93
+ line = strings.TrimSpace(line[5:])
94
+ }
95
+ if line == "" || line == "[DONE]" {
96
+ continue
97
+ }
98
+ var obj map[string]any
99
+ if json.Unmarshal([]byte(line), &obj) == nil {
100
+ if _, ok := obj["result"]; ok {
101
+ out, _ := json.MarshalIndent(obj, "", " ")
102
+ fmt.Println(string(out))
103
+ return
104
+ }
105
+ if _, ok := obj["error"]; ok {
106
+ out, _ := json.MarshalIndent(obj, "", " ")
107
+ fmt.Println(string(out))
108
+ return
109
+ }
110
+ }
111
+ }
112
+ fmt.Println(string(raw))
113
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // Call MCP tool on server '{{ server_name }}'
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const SERVER = "{{ server_name }}";
7
+ const CREDS_FILE = path.join(process.env.HOME, ".mcps", "credentials");
8
+
9
+ function loadCredentials() {
10
+ const text = fs.readFileSync(CREDS_FILE, "utf-8");
11
+ const section = new RegExp(`\\[${SERVER}\\]([\\s\\S]*?)(?=\\n\\[|$)`);
12
+ const match = text.match(section);
13
+ if (!match) {
14
+ console.error(`No section [${SERVER}] in ${CREDS_FILE}`);
15
+ process.exit(1);
16
+ }
17
+ const block = match[1];
18
+ const url = block.match(/^url\s*=\s*(.+)$/m)?.[1]?.trim();
19
+ const token = block.match(/^token\s*=\s*(.+)$/m)?.[1]?.trim();
20
+ if (!url || !token) {
21
+ console.error(`Missing url or token for [${SERVER}]`);
22
+ process.exit(1);
23
+ }
24
+ return { url: url.replace(/\/?$/, "/"), token };
25
+ }
26
+
27
+ async function callTool(toolName, args) {
28
+ const { url, token } = loadCredentials();
29
+ const body = JSON.stringify({
30
+ jsonrpc: "2.0",
31
+ method: "tools/call",
32
+ params: { name: toolName, arguments: args },
33
+ id: 1,
34
+ });
35
+ const resp = await fetch(url, {
36
+ method: "POST",
37
+ headers: {
38
+ Authorization: `Bearer ${token}`,
39
+ "Content-Type": "application/json",
40
+ Accept: "application/json, text/event-stream",
41
+ "MCP-Protocol-Version": "2025-06-18",
42
+ },
43
+ body,
44
+ });
45
+ const text = await resp.text();
46
+ for (const line of text.split("\n")) {
47
+ let data = line.startsWith("data:") ? line.slice(5).trim() : line.trim();
48
+ if (!data || data === "[DONE]") continue;
49
+ try {
50
+ const obj = JSON.parse(data);
51
+ if (obj.result || obj.error) return obj;
52
+ } catch {}
53
+ }
54
+ return JSON.parse(text);
55
+ }
56
+
57
+ const [toolName, rawArgs] = process.argv.slice(2);
58
+ if (!toolName) {
59
+ console.error(`Usage: ${process.argv[1]} <tool_name> [json_args]`);
60
+ process.exit(1);
61
+ }
62
+ const args = rawArgs ? JSON.parse(rawArgs) : {};
63
+ callTool(toolName, args).then((r) => console.log(JSON.stringify(r, null, 2)));
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """Call MCP tool on server '{{ server_name }}'."""
3
+ import configparser
4
+ import json
5
+ import os
6
+ import sys
7
+ import urllib.request
8
+
9
+ SERVER = "{{ server_name }}"
10
+ CREDS_FILE = os.path.expanduser("~/.mcps/credentials")
11
+
12
+
13
+ def load_credentials():
14
+ cfg = configparser.ConfigParser()
15
+ cfg.read(CREDS_FILE)
16
+ if SERVER not in cfg:
17
+ print(f"No section [{SERVER}] in {CREDS_FILE}", file=sys.stderr)
18
+ sys.exit(1)
19
+ return cfg[SERVER]["url"], cfg[SERVER]["token"]
20
+
21
+
22
+ def call_tool(tool_name: str, arguments: dict) -> dict:
23
+ url, token = load_credentials()
24
+ url = url.rstrip("/") + "/"
25
+ body = json.dumps({
26
+ "jsonrpc": "2.0",
27
+ "method": "tools/call",
28
+ "params": {"name": tool_name, "arguments": arguments},
29
+ "id": 1,
30
+ }).encode()
31
+ req = urllib.request.Request(
32
+ url, data=body, method="POST",
33
+ headers={
34
+ "Authorization": f"Bearer {token}",
35
+ "Content-Type": "application/json",
36
+ "Accept": "application/json, text/event-stream",
37
+ "MCP-Protocol-Version": "2025-06-18",
38
+ },
39
+ )
40
+ with urllib.request.urlopen(req, timeout=30) as resp:
41
+ text = resp.read().decode()
42
+ for line in text.strip().split("\n"):
43
+ if line.startswith("data:"):
44
+ line = line[5:].strip()
45
+ if not line or line == "[DONE]":
46
+ continue
47
+ try:
48
+ obj = json.loads(line)
49
+ if "result" in obj or "error" in obj:
50
+ return obj
51
+ except json.JSONDecodeError:
52
+ pass
53
+ return json.loads(text)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ if len(sys.argv) < 2:
58
+ print(f"Usage: {sys.argv[0]} <tool_name> [json_args]", file=sys.stderr)
59
+ sys.exit(1)
60
+ tool = sys.argv[1]
61
+ args = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
62
+ result = call_tool(tool, args)
63
+ print(json.dumps(result, indent=2))
@@ -0,0 +1,101 @@
1
+ // Call MCP tool on server '{{ server_name }}'.
2
+ // Dependencies: ureq, serde_json
3
+ // cargo install cargo-script OR use a Cargo.toml with:
4
+ // [dependencies]
5
+ // ureq = "2"
6
+ // serde_json = "1"
7
+ // Usage: cargo run -- <tool_name> [json_args]
8
+
9
+ use std::env;
10
+ use std::fs;
11
+ use std::path::PathBuf;
12
+ use std::process;
13
+
14
+ const SERVER: &str = "{{ server_name }}";
15
+
16
+ fn creds_path() -> PathBuf {
17
+ let home = env::var("HOME").unwrap_or_else(|_| ".".into());
18
+ PathBuf::from(home).join(".mcps").join("credentials")
19
+ }
20
+
21
+ fn load_credentials() -> (String, String) {
22
+ let text = fs::read_to_string(creds_path()).unwrap_or_else(|e| {
23
+ eprintln!("Cannot read credentials: {}", e);
24
+ process::exit(1);
25
+ });
26
+ let header = format!("[{}]", SERVER);
27
+ let mut in_section = false;
28
+ let (mut url, mut token) = (String::new(), String::new());
29
+ for line in text.lines() {
30
+ let trimmed = line.trim();
31
+ if trimmed == header {
32
+ in_section = true;
33
+ continue;
34
+ }
35
+ if in_section && trimmed.starts_with('[') {
36
+ break;
37
+ }
38
+ if in_section {
39
+ if let Some((k, v)) = trimmed.split_once('=') {
40
+ match k.trim() {
41
+ "url" => url = v.trim().to_string(),
42
+ "token" => token = v.trim().to_string(),
43
+ _ => {}
44
+ }
45
+ }
46
+ }
47
+ }
48
+ if url.is_empty() || token.is_empty() {
49
+ eprintln!("Missing url or token for [{}]", SERVER);
50
+ process::exit(1);
51
+ }
52
+ if !url.ends_with('/') {
53
+ url.push('/');
54
+ }
55
+ (url, token)
56
+ }
57
+
58
+ fn main() {
59
+ let args: Vec<String> = env::args().collect();
60
+ if args.len() < 2 {
61
+ eprintln!("Usage: {} <tool_name> [json_args]", args[0]);
62
+ process::exit(1);
63
+ }
64
+ let tool_name = &args[1];
65
+ let tool_args: serde_json::Value = if args.len() > 2 {
66
+ serde_json::from_str(&args[2]).unwrap_or(serde_json::json!({}))
67
+ } else {
68
+ serde_json::json!({})
69
+ };
70
+ let (url, token) = load_credentials();
71
+ let body = serde_json::json!({
72
+ "jsonrpc": "2.0",
73
+ "method": "tools/call",
74
+ "params": {"name": tool_name, "arguments": tool_args},
75
+ "id": 1
76
+ });
77
+ let resp = ureq::post(&url)
78
+ .set("Authorization", &format!("Bearer {}", token))
79
+ .set("Content-Type", "application/json")
80
+ .set("Accept", "application/json, text/event-stream")
81
+ .set("MCP-Protocol-Version", "2025-06-18")
82
+ .send_string(&body.to_string())
83
+ .unwrap_or_else(|e| {
84
+ eprintln!("Request failed: {}", e);
85
+ process::exit(1);
86
+ });
87
+ let text = resp.into_string().unwrap_or_default();
88
+ for line in text.lines() {
89
+ let data = if line.starts_with("data:") { line[5..].trim() } else { line.trim() };
90
+ if data.is_empty() || data == "[DONE]" {
91
+ continue;
92
+ }
93
+ if let Ok(obj) = serde_json::from_str::<serde_json::Value>(data) {
94
+ if obj.get("result").is_some() || obj.get("error").is_some() {
95
+ println!("{}", serde_json::to_string_pretty(&obj).unwrap());
96
+ return;
97
+ }
98
+ }
99
+ }
100
+ println!("{}", text);
101
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: {{ server_name }}
3
+ description: MCP tools for {{ server_name }}. Available tools: {{ tool_names }}. Use when the user needs to call these MCP tools.
4
+ ---
5
+
6
+ # {{ server_name }} MCP Tools
7
+
8
+ ## Available Tools
9
+ {% for tool in tools %}
10
+
11
+ ### {{ tool.name }}
12
+
13
+ {{ tool.description | default("No description.", true) }}
14
+ {% if tool.params %}
15
+
16
+ **Parameters:**
17
+ {% for p in tool.params %}
18
+ - `{{ p.name }}` ({{ p.type }}, {{ p.required }}): {{ p.description | default("-", true) }}
19
+ {% endfor %}
20
+ {% endif %}
21
+
22
+ **Execute:**
23
+ ```{{ script_lang }}
24
+ {{ script_bin }} {{ skill_path }}/scripts/call.{{ script_ext }} {{ tool.name }}{% if tool.required_params %} '{{ tool.example_args }}'{% endif %}
25
+
26
+ ```
27
+ {% endfor %}
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: {{ server_name }}-{{ tool.name }}
3
+ description: MCP tool {{ tool.name }} from {{ server_name }}. {{ tool.description }}
4
+ ---
5
+
6
+ # {{ tool.name }}
7
+
8
+ {{ tool.description | default("No description.", true) }}
9
+ {% if tool.params %}
10
+
11
+ **Parameters:**
12
+ {% for p in tool.params %}
13
+ - `{{ p.name }}` ({{ p.type }}, {{ p.required }}): {{ p.description | default("-", true) }}
14
+ {% endfor %}
15
+ {% endif %}
16
+
17
+ **Execute:**
18
+ ```{{ script_lang }}
19
+ {{ script_bin }} {{ skill_path }}/scripts/call.{{ script_ext }} {{ tool.name }}{% if tool.required_params %} '{{ tool.example_args }}'{% endif %}
20
+
21
+ ```
@@ -0,0 +1,107 @@
1
+ import json
2
+ import urllib.request
3
+ import tiktoken
4
+
5
+ SERVERS = {
6
+ "dellstore": {
7
+ "url": "http://localhost:8027/mcp/a0504710-203f-4c4b-b864-5e64641d9ed3/",
8
+ "token": "mcp_token_bb1HbX3e-wykRQYziLNaGGYod_qhxrlffUm4DxmMn7w",
9
+ "skill": ".cursor/skills/dellstore/SKILL.md",
10
+ },
11
+ "neo4j-movie": {
12
+ "url": "http://localhost:8032/mcp/8bb059d5-0115-4c08-b994-25c26695b027/",
13
+ "token": "mcp_token_Na7kQzvA2RxhzvAjT9P3Q0rPiaxlIHUnXiPHCoeMrhE",
14
+ "skill": ".cursor/skills/neo4j-movie/SKILL.md",
15
+ },
16
+ }
17
+
18
+ enc = tiktoken.encoding_for_model("gpt-4o")
19
+
20
+
21
+ def count_tokens(text: str) -> int:
22
+ return len(enc.encode(text))
23
+
24
+
25
+ def get_mcp_tools_payload(url: str, token: str) -> str:
26
+ body = json.dumps({
27
+ "jsonrpc": "2.0",
28
+ "method": "tools/list",
29
+ "params": {},
30
+ "id": 1,
31
+ }).encode()
32
+ req = urllib.request.Request(
33
+ url, data=body, method="POST",
34
+ headers={
35
+ "Authorization": f"Bearer {token}",
36
+ "Content-Type": "application/json",
37
+ "Accept": "application/json, text/event-stream",
38
+ "MCP-Protocol-Version": "2025-06-18",
39
+ },
40
+ )
41
+ with urllib.request.urlopen(req, timeout=30) as resp:
42
+ text = resp.read().decode()
43
+ for line in text.strip().split("\n"):
44
+ if line.startswith("data:"):
45
+ line = line[5:].strip()
46
+ if not line or line == "[DONE]":
47
+ continue
48
+ try:
49
+ obj = json.loads(line)
50
+ if "result" in obj:
51
+ return json.dumps(obj["result"]["tools"], indent=2)
52
+ except json.JSONDecodeError:
53
+ pass
54
+ return text
55
+
56
+
57
+ def main():
58
+ print("=" * 60)
59
+ print("MCP Tools vs Skills — Token Comparison")
60
+ print("=" * 60)
61
+
62
+ mcp_payloads = {}
63
+ skill_payloads = {}
64
+
65
+ for name, cfg in SERVERS.items():
66
+ print(f"\n--- {name} ---")
67
+
68
+ mcp_json = get_mcp_tools_payload(cfg["url"], cfg["token"])
69
+ mcp_payloads[name] = mcp_json
70
+
71
+ with open(cfg["skill"]) as f:
72
+ skill_md = f.read()
73
+ skill_payloads[name] = skill_md
74
+
75
+ mcp_tokens = count_tokens(mcp_json)
76
+ skill_tokens = count_tokens(skill_md)
77
+ saved = mcp_tokens - skill_tokens
78
+ pct = (saved / mcp_tokens * 100) if mcp_tokens else 0
79
+
80
+ print(f" MCP JSON schema tokens : {mcp_tokens:>6}")
81
+ print(f" Skill markdown tokens : {skill_tokens:>6}")
82
+ print(f" Savings : {saved:>6} ({pct:.1f}%)")
83
+
84
+ print("\n" + "=" * 60)
85
+ print("Multi-server scenario (all servers active)")
86
+ print("=" * 60)
87
+
88
+ all_mcp = "\n".join(mcp_payloads.values())
89
+ all_mcp_tokens = count_tokens(all_mcp)
90
+
91
+ print(f"\n Traditional MCP (all tools every turn) : {all_mcp_tokens:>6} tokens")
92
+
93
+ for name, skill_md in skill_payloads.items():
94
+ t = count_tokens(skill_md)
95
+ saved = all_mcp_tokens - t
96
+ pct = (saved / all_mcp_tokens * 100) if all_mcp_tokens else 0
97
+ print(f" Skill '{name}' only : {t:>6} tokens (saves {saved}, {pct:.1f}%)")
98
+
99
+ all_skills = "\n".join(skill_payloads.values())
100
+ all_skills_tokens = count_tokens(all_skills)
101
+ saved = all_mcp_tokens - all_skills_tokens
102
+ pct = (saved / all_mcp_tokens * 100) if all_mcp_tokens else 0
103
+ print(f" All skills loaded (worst case) : {all_skills_tokens:>6} tokens (saves {saved}, {pct:.1f}%)")
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()