initial checkin

This commit is contained in:
Markus Pfundstein
2024-11-29 12:07:37 +01:00
commit 0a804c6cc9
5 changed files with 291 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

97
README.md Normal file
View File

@@ -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`
<details>
<summary>Development/Unpublished Servers Configuration</summary>
```
"mcpServers": {
"mcp-knowledge-base": {
"command": "uv",
"args": [
"--directory",
"/Users/$(whoami)/experiments/claude-mvp/mcp-knowledge-base",
"run",
"mcp-knowledge-base"
]
}
}
```
</details>
<details>
<summary>Published Servers Configuration</summary>
```
"mcpServers": {
"mcp-knowledge-base": {
"command": "uvx",
"args": [
"mcp-knowledge-base"
]
}
}
```
</details>
## 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.

21
pyproject.toml Normal file
View File

@@ -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"

View File

@@ -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']

View File

@@ -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()
)