commit 0a804c6cc921e6a38b2b7bc62cd8ab729c4f3fcd Author: Markus Pfundstein Date: Fri Nov 29 12:07:37 2024 +0100 initial checkin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9c0c1b --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# mcp-knowledge-base MCP server + +Example MCP server to call command line apps + +## Components + +### Tools + +The server implements one tool: +- run_command: Runs a command line comment + - Takes "cmd" and "args" as string arguments + - Runs the command and returns stdout, stderr, status_code, etc. + +## Configuration + + +## Quickstart + +### Install + +#### Claude Desktop + +On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` +On Windows: `%APPDATA%/Claude/claude_desktop_config.json` + +
+ Development/Unpublished Servers Configuration + ``` + "mcpServers": { + "mcp-knowledge-base": { + "command": "uv", + "args": [ + "--directory", + "/Users/$(whoami)/experiments/claude-mvp/mcp-knowledge-base", + "run", + "mcp-knowledge-base" + ] + } + } + ``` +
+ +
+ Published Servers Configuration + ``` + "mcpServers": { + "mcp-knowledge-base": { + "command": "uvx", + "args": [ + "mcp-knowledge-base" + ] + } + } + ``` +
+ +## Development + +### Building and Publishing + +To prepare the package for distribution: + +1. Sync dependencies and update lockfile: +```bash +uv sync +``` + +2. Build package distributions: +```bash +uv build +``` + +This will create source and wheel distributions in the `dist/` directory. + +3. Publish to PyPI: +```bash +uv publish +``` + +Note: You'll need to set PyPI credentials via environment variables or command flags: +- Token: `--token` or `UV_PUBLISH_TOKEN` +- Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD` + +### Debugging + +Since MCP servers run over stdio, debugging can be challenging. For the best debugging +experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). + + +You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command: + +```bash +npx @modelcontextprotocol/inspector uv --directory /Users/markus/experiments/claude-mvp/mcp-knowledge-base run mcp-knowledge-base +``` + + +Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..483e7ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "mcp-knowledge-base" +version = "0.1.0" +description = "Example MCP server to create a knowledge-base" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "mcp>=1.0.0", + "python-dotenv>=1.0.1", + "requests>=2.32.3", +] +[[project.authors]] +name = "Markus Pfundstein" +email = "markus@life-electronic.nl" + +[build-system] +requires = [ "hatchling",] +build-backend = "hatchling.build" + +[project.scripts] +mcp-knowledge-base = "mcp_knowledge_base:main" diff --git a/src/mcp_knowledge_base/__init__.py b/src/mcp_knowledge_base/__init__.py new file mode 100644 index 0000000..6217b1f --- /dev/null +++ b/src/mcp_knowledge_base/__init__.py @@ -0,0 +1,9 @@ +from . import server +import asyncio + +def main(): + """Main entry point for the package.""" + asyncio.run(server.main()) + +# Optionally expose other important items at package level +__all__ = ['main', 'server'] \ No newline at end of file diff --git a/src/mcp_knowledge_base/server.py b/src/mcp_knowledge_base/server.py new file mode 100644 index 0000000..c959251 --- /dev/null +++ b/src/mcp_knowledge_base/server.py @@ -0,0 +1,154 @@ + +import json +import logging +from collections.abc import Sequence +from functools import lru_cache +from typing import Any + +import subprocess +from dotenv import load_dotenv +from mcp.server import Server +import asyncio +from mcp.types import ( + + Tool, + TextContent, + ImageContent, + EmbeddedResource, + LoggingLevel +) +from pydantic import AnyUrl + +# Load environment variables +load_dotenv() + +api_key = "x" + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("mcp-knowledge-base") + +app = Server("mcp-knowledge-base") + +TOOL_LIST_FILES_IN_VAULT = "list_files_in_vault" +TOOL_LIST_FILES_IN_DIR = "list_files_in_dir" + +@app.list_tools() +async def list_tools() -> list[Tool]: + """List available tools.""" + return [ + Tool( + name="list_files_in_vault", + description="Lists all files and directories in the root directory of your Obsidian vault.", + inputSchema={ + "type": "object", + "properties": {}, + "required": [] + }, + ), + Tool( + name="list_files_in_dir", + description="Lists all files and directories that exist in a specific Obsidian directory.", + inputSchema={ + "type": "object", + "properties": { + "dirpath": { + "type": "string", + "description": "Path to list files from (relative to your vault root). Note that empty directories will not be returned." + }, + }, + "required": ["dirpath"] + } + ) + ] + +class ToolHandler(): + def __init__(self, tool_name: str): + self.name = tool_name + + def run_tool(self, args: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + pass + +class ListFilesInVaultToolHandler(ToolHandler): + def __init__(self): + super().__init__(TOOL_LIST_FILES_IN_VAULT) + + def run_tool(self, args: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + + files = [ + "a.txt", + "b.txt", + "c/" + ] + + return [ + TextContent( + type="text", + text=json.dumps(files, indent=2) + ) + ] + +class ListFilesInDirToolHandler(ToolHandler): + def __init__(self): + super().__init__(TOOL_LIST_FILES_IN_DIR) + + def run_tool(self, args: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + + files = [ + "a.txt", + "b.txt", + "c/" + ] + + return [ + TextContent( + type="text", + text=json.dumps(files, indent=2) + ) + ] + +tool_handlers = {} +def add_tool_handler(tool_class: ToolHandler): + global tool_handlers + + tool_handlers[tool_class.name] = tool_class + +def get_tool_handler(name: str) -> ToolHandler | None: + if name not in tool_handlers: + return None + + return tool_handlers[name] + +@app.call_tool() +async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]: + """Handle tool calls for command line run.""" + + add_tool_handler(ListFilesInDirToolHandler()) + add_tool_handler(ListFilesInVaultToolHandler()) + + + tool_handler = get_tool_handler(name) + if not tool_handler: + raise ValueError(f"Unknown tool: {name}") + + try: + return tool_handler.run_tool(arguments) + except Exception as e: + logger.error(f"Error: {str(e)}") + raise RuntimeError(f"Error: {str(e)}") + + +async def main(): + + # Import here to avoid issues with event loops + from mcp.server.stdio import stdio_server + + async with stdio_server() as (read_stream, write_stream): + await app.run( + read_stream, + write_stream, + app.create_initialization_options() + ) + + +