mcp
This commit is contained in:
@@ -9,6 +9,7 @@ authors = [
|
|||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.141.1",
|
"fastapi>=0.141.1",
|
||||||
|
"fastmcp>=3.4.7",
|
||||||
"ipython>=9.16.1",
|
"ipython>=9.16.1",
|
||||||
"matplotlib>=3.11.1",
|
"matplotlib>=3.11.1",
|
||||||
"matplotlib-inline>=0.1.7",
|
"matplotlib-inline>=0.1.7",
|
||||||
@@ -18,6 +19,8 @@ dependencies = [
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
ipython-demo = "ipython_demo.app:main"
|
ipython-demo = "ipython_demo.app:main"
|
||||||
|
ipython-webapp = "ipython_webapp.app:main"
|
||||||
|
ipython-mcp = "ipython_mcp.app:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.12.6,<0.13.0"]
|
requires = ["uv_build>=0.12.6,<0.13.0"]
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# FastMCP is not FastAPI: this module exposes MCP tools, not HTTP routes.
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
import ipython_demo
|
||||||
|
from ipython_demo.models import CallOptions, CallResponse, ShellInfo
|
||||||
|
|
||||||
|
app = FastMCP(name="IPython Demo")
|
||||||
|
shell_app = ipython_demo.App()
|
||||||
|
|
||||||
|
|
||||||
|
@app.tool
|
||||||
|
def run_code(
|
||||||
|
code: str,
|
||||||
|
shell_name: Literal["last", "new"] | str = "last",
|
||||||
|
options: CallOptions | None = None,
|
||||||
|
) -> CallResponse:
|
||||||
|
"""Run code in a named shell and return a frontend-facing call response.
|
||||||
|
|
||||||
|
use shell_name="new" to create a new shell, or shell_name="last" to use the most recently selected shell. Otherwise, use an existing shell name to run code in that shell.
|
||||||
|
"""
|
||||||
|
return shell_app.run_code(code, shell_name, options=options)
|
||||||
|
|
||||||
|
|
||||||
|
@app.tool
|
||||||
|
def list_shells() -> list[ShellInfo]:
|
||||||
|
"""Return frontend-safe metadata for all known shells."""
|
||||||
|
return shell_app.list_shells()
|
||||||
|
|
||||||
|
|
||||||
|
@app.tool
|
||||||
|
def get_shell(shell_name: Literal["last"] | str) -> ShellInfo:
|
||||||
|
"""Return frontend-safe metadata for a single shell."""
|
||||||
|
return (
|
||||||
|
shell_app.get_last_shell()
|
||||||
|
if shell_name == "last"
|
||||||
|
else shell_app.get_shell(shell_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.tool
|
||||||
|
def create_shell() -> ShellInfo:
|
||||||
|
"""Create a fresh named shell and return its metadata."""
|
||||||
|
return shell_app.create_shell()
|
||||||
|
|
||||||
|
|
||||||
|
@app.tool
|
||||||
|
def get_last_shell() -> ShellInfo:
|
||||||
|
"""
|
||||||
|
Return metadata for the most recently selected shell.
|
||||||
|
|
||||||
|
get_shell('last') is equivalent, but this is a more explicit name.
|
||||||
|
"""
|
||||||
|
return shell_app.get_last_shell()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run the MCP server over FastMCP's default transport."""
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -39,7 +39,9 @@ def get_shell(shell_name: str):
|
|||||||
@app.post("/shells/{shell_name}/run")
|
@app.post("/shells/{shell_name}/run")
|
||||||
def run_code(
|
def run_code(
|
||||||
shell_name: Annotated[str, Path(..., description="The shell to run code in")],
|
shell_name: Annotated[str, Path(..., description="The shell to run code in")],
|
||||||
code: Annotated[str, Body(..., description="The code to execute", media_type="text/plain")],
|
code: Annotated[
|
||||||
|
str, Body(..., description="The code to execute", media_type="text/plain")
|
||||||
|
],
|
||||||
options: Annotated[CallOptions, Depends()],
|
options: Annotated[CallOptions, Depends()],
|
||||||
):
|
):
|
||||||
return ShellApp.run_code(code, shell_name, options=options)
|
return ShellApp.run_code(code, shell_name, options=options)
|
||||||
@@ -47,13 +49,21 @@ def run_code(
|
|||||||
|
|
||||||
@app.post("/run")
|
@app.post("/run")
|
||||||
def run_code_2(
|
def run_code_2(
|
||||||
code: Annotated[str, Body(..., description="The code to execute", media_type="text/plain")],
|
code: Annotated[
|
||||||
|
str, Body(..., description="The code to execute", media_type="text/plain")
|
||||||
|
],
|
||||||
options: Annotated[CallOptions, Depends()],
|
options: Annotated[CallOptions, Depends()],
|
||||||
shell_name: Annotated[str, Query(description="The shell to run code in")] = "last",
|
shell_name: Annotated[str, Query(description="The shell to run code in")] = "last",
|
||||||
):
|
):
|
||||||
return ShellApp.run_code(code, shell_name, options=options)
|
return ShellApp.run_code(code, shell_name, options=options)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run the HTTP API with Uvicorn."""
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
uvicorn.run(app, host="::", port=8000, log_level="info")
|
uvicorn.run(app, host="::", port=8000, log_level="info")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
from ipython_mcp.app import app
|
||||||
|
|
||||||
|
|
||||||
|
class MCPAppTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_exports_typed_ipython_tools_from_plain_fastmcp(self):
|
||||||
|
self.assertIsInstance(app, FastMCP)
|
||||||
|
|
||||||
|
tools = await app.list_tools()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{tool.name for tool in tools},
|
||||||
|
{
|
||||||
|
"run_code",
|
||||||
|
"list_shells",
|
||||||
|
"get_shell",
|
||||||
|
"create_shell",
|
||||||
|
"get_last_shell",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user