initial checkin
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
97
README.md
Normal file
97
README.md
Normal 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
21
pyproject.toml
Normal 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"
|
9
src/mcp_knowledge_base/__init__.py
Normal file
9
src/mcp_knowledge_base/__init__.py
Normal 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']
|
154
src/mcp_knowledge_base/server.py
Normal file
154
src/mcp_knowledge_base/server.py
Normal 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()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user