Skip to content

Commit b460d3a

Browse files
committed
initial MCP support
1 parent fe90333 commit b460d3a

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

extras/chatbot.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import asyncio
2+
from mcp import ClientSession, StdioServerParameters
3+
from mcp.client.stdio import stdio_client
4+
5+
from langchain_openai import ChatOpenAI
6+
from langchain_mcp_adapters.tools import load_mcp_tools
7+
from langgraph.prebuilt import create_react_agent
8+
9+
import sys
10+
11+
model = ChatOpenAI(model='granite3.2:latest', base_url='http://127.0.0.1:4000/v1', api_key='xxx')
12+
13+
server_params = StdioServerParameters(command="kmcp.py")
14+
15+
16+
async def main_chat():
17+
async with stdio_client(server_params) as (read, write):
18+
async with ClientSession(read, write) as session:
19+
await session.initialize()
20+
tools = await load_mcp_tools(session)
21+
agent = create_react_agent(model, tools)
22+
23+
print("Terminal chatbot is ready. Type 'exit' to quit.")
24+
chat_history = []
25+
26+
while True:
27+
user_input = input("You: ").strip()
28+
if user_input.lower() in {"exit", "quit"}:
29+
print("Exiting chatbot.")
30+
break
31+
32+
chat_history.append({"role": "user", "content": user_input})
33+
34+
while True:
35+
result = await agent.ainvoke({"messages": chat_history})
36+
last_message = result["messages"][-1]
37+
38+
# Check if it's a tool_use message
39+
if hasattr(last_message, 'type') and last_message.type == "tool_use":
40+
# Agent wants to call a tool → MCP server handles it automatically
41+
tool_name = last_message.tool_call[0]['name']
42+
tool_args = last_message.tool_call[0]['args']
43+
print(f"[Agent is calling tool '{tool_name}' with args {tool_args}]")
44+
45+
# Append tool_use message to chat history so agent remembers it
46+
chat_history.append({"role": "user", "content": f"Calling tool: {tool_name}"})
47+
continue # Run agent again to get tool result processed
48+
49+
# Otherwise — final bot response
50+
bot_reply = last_message.content
51+
print(f"Bot: {bot_reply}")
52+
chat_history.append({"role": "assistant", "content": bot_reply})
53+
break
54+
55+
56+
if __name__ == "__main__":
57+
try:
58+
asyncio.run(main_chat())
59+
except KeyboardInterrupt:
60+
print("\nChatbot interrupted. Exiting.")
61+
sys.exit(0)

kvirt/mcp.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from mcp.server.fastmcp import FastMCP
2+
from kvirt.config import Kbaseconfig, Kconfig
3+
from urllib.request import urlopen
4+
5+
mcp = FastMCP("kclimcp")
6+
7+
8+
@mcp.prompt()
9+
def prompt() -> str:
10+
"""Indicates contexts of questions related to kcli"""
11+
return """You are a helpful assistant who knows everything about kcli, a powerful client and library written
12+
in Python and meant to interact with different virtualization providers, easily deploy and customize VMs or
13+
full kubernetes/OpenShift clusters. All information about kcli is available at
14+
https://github.com/karmab/kcli/blob/main/docs/index.md"""
15+
16+
17+
@mcp.resource("resource://kcli-doc.md")
18+
def get_doc() -> str:
19+
"""Provides kcli documentation"""
20+
url = 'https://raw.githubusercontent.com/karmab/kcli/refs/heads/main/docs/index.md'
21+
return urlopen(url).read().decode('utf-8')
22+
23+
24+
@mcp.tool()
25+
def about_kcli() -> str:
26+
"""What is kcli"""
27+
return open('about.txt').read()
28+
29+
30+
@mcp.tool()
31+
def list_clients() -> list:
32+
"""List kcli clients/providers"""
33+
clientstable = ["Client", "Type", "Enabled", "Current"]
34+
baseconfig = Kbaseconfig()
35+
for client in sorted(baseconfig.clients):
36+
enabled = baseconfig.ini[client].get('enabled', True)
37+
_type = baseconfig.ini[client].get('type', 'kvm')
38+
if client == baseconfig.client:
39+
clientstable.append([client, _type, enabled, 'X'])
40+
else:
41+
clientstable.append([client, _type, enabled, ''])
42+
return clientstable
43+
44+
45+
@mcp.tool()
46+
def list_vms(client: str = None) -> list:
47+
"""List kcli vms for specific client or for default one when unspecified"""
48+
return Kconfig(client).k.list()
49+
50+
51+
@mcp.tool()
52+
def info_vm(name: str, client: str = None) -> dict:
53+
"""Get info of a kcli vm"""
54+
return Kconfig(client).k.info(name)
55+
56+
57+
@mcp.tool()
58+
def create_vm(name: str, profile: str, overrides: dict, client: str = None) -> dict:
59+
"""Create a kcli vm"""
60+
return Kconfig(client).create_vm(name, profile, overrides=overrides)
61+
62+
63+
@mcp.tool()
64+
def delete_vm(vm: str, client: str = None) -> dict:
65+
"""Delete a kcli vm"""
66+
return Kconfig(client).k.delete(vm)
67+
68+
69+
def main():
70+
mcp.run(transport="stdio")
71+
72+
73+
if __name__ == "__main__":
74+
main()

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
kcli=kvirt.cli:cli
5858
kweb=kvirt.web.main:run
5959
klist.py=kvirt.klist:main
60+
kmcp.py=kvirt.mcp:main
6061
ksushy=kvirt.ksushy.main:run
6162
ignitionmerger=kvirt.ignitionmerger:cli
6263
ekstoken=kvirt.ekstoken:cli

0 commit comments

Comments
 (0)