Skip to content

Commit 285fc83

Browse files
mdesmetsaravmajesticanandgupta42
authored
feat: add mcp command group with create-mcp-proxy functionality (#52)
* feat: add mcp command group with create-mcp-proxy functionality * feat: add MCP proxy config processing with inputs and servers * feat: add MCP integration and tool listing * feat: add JSON config parsing with input token replacement * refactor: rename command and improve tool list structure * chore: format and organize imports in mcp utils * feat: dynamically discover input tokens from config * feat: include server name in JSON output * feat: prompt user to select server when multiple configured * feat: add clipboard support for JSON output * feat: prompt only for inputs used by selected server * feat: add InputParameter dataclass for MCP utils * feat: add input parameters config to command output * refactor: hardcode password input settings in mcp proxy * feat: copy JSON output to clipboard with notification * feat: improve MCP server config handling and error reporting * feat: add error handling for MCP server connection * feat: add command args and env with config placeholders to output * feat: validate MCP server type is stdio * fix: add server_config param to list_tools function * fix: normalize input tokens and preserve all server config args/env * refactor: simplify code formatting and improve string formatting * fix: env variables as array * fix: Add exception chaining in mcp.py and make setup.py executable * ci: remove Python 3.9 and PyPy3.9 test jobs * Bump version: 0.0.18 → 0.0.19 --------- Co-authored-by: Saravanan S <[email protected]> Co-authored-by: anandgupta42 <[email protected]>
1 parent e652074 commit 285fc83

File tree

9 files changed

+187
-64
lines changed

9 files changed

+187
-64
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.0.18
2+
current_version = 0.0.19
33
commit = True
44
tag = True
55

.github/workflows/github-actions.yml

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,6 @@ jobs:
1919
toxpython: 'python3.11'
2020
tox_env: 'docs'
2121
os: 'ubuntu-latest'
22-
- name: 'py39-pydantic28-cover (ubuntu)'
23-
python: '3.9'
24-
toxpython: 'python3.9'
25-
python_arch: 'x64'
26-
tox_env: 'py39-pydantic28-cover'
27-
os: 'ubuntu-latest'
28-
- name: 'py39-pydantic28-cover (windows)'
29-
python: '3.9'
30-
toxpython: 'python3.9'
31-
python_arch: 'x64'
32-
tox_env: 'py39-pydantic28-cover'
33-
os: 'windows-latest'
34-
- name: 'py39-pydantic28-cover (macos)'
35-
python: '3.9'
36-
toxpython: 'python3.9'
37-
python_arch: 'x64'
38-
tox_env: 'py39-pydantic28-cover'
39-
os: 'macos-13'
40-
- name: 'py39-pydantic210-cover'
41-
python: '3.9'
42-
toxpython: 'python3.9'
43-
python_arch: 'x64'
44-
tox_env: 'py39-pydantic210-cover'
45-
os: 'ubuntu-latest'
46-
- name: 'py39-pydantic28-nocov'
47-
python: '3.9'
48-
toxpython: 'python3.9'
49-
python_arch: 'x64'
50-
tox_env: 'py39-pydantic28-nocov'
51-
os: 'ubuntu-latest'
52-
- name: 'py39-pydantic210-nocov'
53-
python: '3.9'
54-
toxpython: 'python3.9'
55-
python_arch: 'x64'
56-
tox_env: 'py39-pydantic210-nocov'
57-
os: 'ubuntu-latest'
5822
- name: 'py310-pydantic28-cover'
5923
python: '3.10'
6024
toxpython: 'python3.10'
@@ -127,30 +91,6 @@ jobs:
12791
python_arch: 'x64'
12892
tox_env: 'py312-pydantic210-nocov'
12993
os: 'ubuntu-latest'
130-
- name: 'pypy39-pydantic28-cover'
131-
python: 'pypy-3.9'
132-
toxpython: 'pypy3.9'
133-
python_arch: 'x64'
134-
tox_env: 'pypy39-pydantic28-cover'
135-
os: 'ubuntu-latest'
136-
- name: 'pypy39-pydantic210-cover'
137-
python: 'pypy-3.9'
138-
toxpython: 'pypy3.9'
139-
python_arch: 'x64'
140-
tox_env: 'pypy39-pydantic210-cover'
141-
os: 'ubuntu-latest'
142-
- name: 'pypy39-pydantic28-nocov'
143-
python: 'pypy-3.9'
144-
toxpython: 'pypy3.9'
145-
python_arch: 'x64'
146-
tox_env: 'pypy39-pydantic28-nocov'
147-
os: 'ubuntu-latest'
148-
- name: 'pypy39-pydantic210-nocov'
149-
python: 'pypy-3.9'
150-
toxpython: 'pypy3.9'
151-
python_arch: 'x64'
152-
tox_env: 'pypy39-pydantic210-nocov'
153-
os: 'ubuntu-latest'
15494
- name: 'pypy310-pydantic28-cover'
15595
python: 'pypy-3.10'
15696
toxpython: 'pypy3.10'

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ docs/_build
7272

7373
# Mypy Cache
7474
.mypy_cache/
75+
.aider*

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
year = "2024"
1616
author = "Altimate Inc."
1717
copyright = f"{year}, {author}"
18-
version = release = "0.0.18"
18+
version = release = "0.0.19"
1919

2020
pygments_style = "trac"
2121
templates_path = ["."]

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def read(*names, **kwargs):
1313

1414
setup(
1515
name="altimate-datapilot-cli",
16-
version="0.0.18",
16+
version="0.0.19",
1717
license="MIT",
1818
description="Assistant for Data Teams",
1919
long_description="{}\n{}".format(
@@ -68,6 +68,8 @@ def read(*names, **kwargs):
6868
"tabulate~=0.9.0",
6969
"requests>=2.31",
7070
"sqlglot~=25.30.0",
71+
"mcp~=1.9.0",
72+
"pyperclip~=1.8.2",
7173
],
7274
extras_require={
7375
# eg:

src/datapilot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.18"
1+
__version__ = "0.0.19"

src/datapilot/cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import click
22

3+
from datapilot.core.mcp_utils.mcp import mcp
34
from datapilot.core.platforms.dbt.cli.cli import dbt
45

56

@@ -9,3 +10,4 @@ def datapilot():
910

1011

1112
datapilot.add_command(dbt)
13+
datapilot.add_command(mcp)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DBT = []
2+
SQL = []
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import asyncio
2+
import json
3+
import logging
4+
import shutil
5+
from dataclasses import dataclass
6+
7+
import click
8+
import pyperclip
9+
from mcp import ClientSession
10+
from mcp import StdioServerParameters
11+
from mcp.client.stdio import stdio_client
12+
13+
logging.basicConfig(level=logging.INFO)
14+
15+
16+
@dataclass
17+
class InputParameter:
18+
name: str
19+
type: str
20+
required: bool
21+
key: str
22+
description: str
23+
24+
25+
def find_input_tokens(data):
26+
tokens = set()
27+
if isinstance(data, list):
28+
for item in data:
29+
tokens.update(find_input_tokens(item))
30+
elif isinstance(data, dict):
31+
for value in data.values():
32+
tokens.update(find_input_tokens(value))
33+
elif isinstance(data, str) and data.startswith("${input:"):
34+
tokens.add(data[8:-1].strip())
35+
return tokens
36+
37+
38+
# New mcp group
39+
@click.group()
40+
def mcp():
41+
"""mcp specific commands."""
42+
43+
44+
@mcp.command("inspect-mcp-server")
45+
def create_mcp_proxy():
46+
content = click.edit()
47+
if content is None:
48+
click.echo("No input provided.")
49+
return
50+
51+
try:
52+
config = json.loads(content)
53+
except json.JSONDecodeError:
54+
click.echo("Invalid JSON content.")
55+
return
56+
57+
inputs = {}
58+
mcp_config = config.get("mcp", {})
59+
60+
# Select server
61+
# Support both "servers" and "mcpServers" naming conventions
62+
servers = mcp_config.get("mcpServers", mcp_config.get("servers", {}))
63+
server_names = list(servers.keys())
64+
65+
if not server_names:
66+
ctx = click.get_current_context()
67+
click.secho("Error: No servers configured in mcp config (tried keys: 'mcpServers' and 'servers')", fg="red")
68+
ctx.exit(1)
69+
70+
if len(server_names) > 1:
71+
server_name = click.prompt("Choose a server", type=click.Choice(server_names), show_choices=True)
72+
else:
73+
server_name = server_names[0]
74+
75+
if server_name in servers:
76+
server_config = servers[server_name]
77+
78+
# Collect input tokens ONLY from this server's config
79+
input_ids = find_input_tokens(server_config.get("args", []))
80+
input_ids.update(find_input_tokens(server_config.get("env", {})))
81+
82+
# Create prompt definitions using BOTH discovered tokens AND configured inputs
83+
existing_input_ids = {i["id"] for i in mcp_config.get("inputs", [])}
84+
inputs_to_prompt = input_ids.intersection(existing_input_ids)
85+
inputs_to_prompt.update(input_ids) # Add any undiscovered-by-config inputs
86+
87+
input_configs = []
88+
for input_id in inputs_to_prompt:
89+
input_def = next((d for d in mcp_config.get("inputs", []) if d["id"] == input_id), {})
90+
inputs[input_id] = click.prompt(
91+
input_def.get("description", input_id),
92+
hide_input=True,
93+
)
94+
# Create InputParameters config entry
95+
input_configs.append(
96+
InputParameter(
97+
name=input_def.get("name", input_id),
98+
type="password",
99+
required=True,
100+
key=input_id,
101+
description=input_def.get("description", ""),
102+
).__dict__
103+
)
104+
105+
# Replace input tokens in args
106+
processed_args = [
107+
inputs.get(arg[8:-1], arg) if isinstance(arg, str) and arg.startswith("${input:") else arg
108+
for arg in server_config.get("args", [])
109+
]
110+
111+
# Replace input tokens in environment variables
112+
processed_env = {
113+
k: inputs.get(v[8:-1], v) if isinstance(v, str) and v.startswith("${input:") else v
114+
for k, v in server_config.get("env", {}).items()
115+
}
116+
117+
# Execute with processed parameters
118+
output = asyncio.run(
119+
list_tools(server_config=server_config, command=server_config["command"], args=processed_args, env=processed_env)
120+
)
121+
# Add processed parameters to output
122+
output_with_name = {
123+
"name": server_name,
124+
"config": input_configs,
125+
"command": server_config["command"],
126+
"args": [arg.replace("${input:", "${") if isinstance(arg, str) else arg for arg in server_config.get("args", [])],
127+
"env": [
128+
{"key": k, "value": v.replace("${input:", "${") if isinstance(v, str) else v}
129+
for k, v in server_config.get("env", {}).items()
130+
],
131+
**output,
132+
}
133+
output_json = json.dumps(output_with_name, indent=2)
134+
click.echo(output_json)
135+
try:
136+
pyperclip.copy(output_json)
137+
click.secho("\nOutput copied to clipboard!", fg="green")
138+
except pyperclip.PyperclipException as e:
139+
click.secho(f"\nFailed to copy to clipboard: {e!s}", fg="yellow")
140+
141+
142+
async def list_tools(server_config: dict, command: str, args: list[str], env: dict[str, str]):
143+
command_path = shutil.which(command)
144+
if not command_path:
145+
raise click.UsageError(f"Command not found: {command}")
146+
147+
try:
148+
# Only support stdio server type
149+
server_type = server_config.get("type", "stdio")
150+
if server_type != "stdio":
151+
raise click.UsageError(f"Only stdio MCP servers are supported. Found type: {server_type}")
152+
153+
server_params = StdioServerParameters(
154+
command=command_path,
155+
args=args,
156+
env=env,
157+
)
158+
159+
async with stdio_client(server_params) as (read, write):
160+
async with ClientSession(read, write) as session:
161+
await session.initialize()
162+
tools = await session.list_tools()
163+
mcp_tools = [
164+
{
165+
"name": tool.name,
166+
"description": tool.description,
167+
"inputSchema": tool.inputSchema,
168+
}
169+
for tool in tools.tools
170+
]
171+
172+
return {
173+
"tools": mcp_tools,
174+
}
175+
except Exception as e:
176+
raise click.UsageError("Could not connect to MCP server: " + str(e)) from e

0 commit comments

Comments
 (0)