# Directory Structure ``` ├── .python-version ├── pyproject.toml ├── README.md ├── src │ └── mcp_server_on_raspi │ ├── __init__.py │ └── server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.13 2 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # mcp-server-on-raspi MCP server 2 | 3 | A MCP server project 4 | 5 | ## Components 6 | 7 | ### Resources 8 | 9 | The server implements a simple note storage system with: 10 | - Custom note:// URI scheme for accessing individual notes 11 | - Each note resource has a name, description and text/plain mimetype 12 | 13 | ### Prompts 14 | 15 | The server provides a single prompt: 16 | - summarize-notes: Creates summaries of all stored notes 17 | - Optional "style" argument to control detail level (brief/detailed) 18 | - Generates prompt combining all current notes with style preference 19 | 20 | ### Tools 21 | 22 | The server implements one tool: 23 | - add-note: Adds a new note to the server 24 | - Takes "name" and "content" as required string arguments 25 | - Updates server state and notifies clients of resource changes 26 | 27 | ## Configuration 28 | 29 | [TODO: Add configuration details specific to your implementation] 30 | 31 | ## Quickstart 32 | 33 | ### Install 34 | 35 | #### Claude Desktop 36 | 37 | On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` 38 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 39 | 40 | <details> 41 | <summary>Development/Unpublished Servers Configuration</summary> 42 | ``` 43 | "mcpServers": { 44 | "mcp-server-on-raspi": { 45 | "command": "uv", 46 | "args": [ 47 | "--directory", 48 | "/Users/daikiwatanabe/ghq/github.com/daikw/mcp-server-on-raspi", 49 | "run", 50 | "mcp-server-on-raspi" 51 | ] 52 | } 53 | } 54 | ``` 55 | </details> 56 | 57 | <details> 58 | <summary>Published Servers Configuration</summary> 59 | ``` 60 | "mcpServers": { 61 | "mcp-server-on-raspi": { 62 | "command": "uvx", 63 | "args": [ 64 | "mcp-server-on-raspi" 65 | ] 66 | } 67 | } 68 | ``` 69 | </details> 70 | 71 | ## Development 72 | 73 | ### Building and Publishing 74 | 75 | To prepare the package for distribution: 76 | 77 | 1. Sync dependencies and update lockfile: 78 | ```bash 79 | uv sync 80 | ``` 81 | 82 | 2. Build package distributions: 83 | ```bash 84 | uv build 85 | ``` 86 | 87 | This will create source and wheel distributions in the `dist/` directory. 88 | 89 | 3. Publish to PyPI: 90 | ```bash 91 | uv publish 92 | ``` 93 | 94 | Note: You'll need to set PyPI credentials via environment variables or command flags: 95 | - Token: `--token` or `UV_PUBLISH_TOKEN` 96 | - Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD` 97 | 98 | ### Debugging 99 | 100 | Since MCP servers run over stdio, debugging can be challenging. For the best debugging 101 | experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). 102 | 103 | 104 | You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command: 105 | 106 | ```bash 107 | npx @modelcontextprotocol/inspector uv --directory /Users/daikiwatanabe/ghq/github.com/daikw/mcp-server-on-raspi run mcp-server-on-raspi 108 | ``` 109 | 110 | 111 | Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging. ``` -------------------------------------------------------------------------------- /src/mcp_server_on_raspi/__init__.py: -------------------------------------------------------------------------------- ```python 1 | from . import server 2 | import asyncio 3 | 4 | def main(): 5 | """Main entry point for the package.""" 6 | asyncio.run(server.main()) 7 | 8 | # Optionally expose other important items at package level 9 | __all__ = ['main', 'server'] ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-server-on-raspi" 3 | version = "0.1.0" 4 | description = "A MCP server project" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ "mcp>=1.1.0",] 8 | [[project.authors]] 9 | name = "daikw" 10 | email = "[email protected]" 11 | 12 | [build-system] 13 | requires = [ "hatchling",] 14 | build-backend = "hatchling.build" 15 | 16 | [project.scripts] 17 | mcp-server-on-raspi = "mcp_server_on_raspi:main" 18 | ``` -------------------------------------------------------------------------------- /src/mcp_server_on_raspi/server.py: -------------------------------------------------------------------------------- ```python 1 | import asyncio 2 | 3 | from mcp.server.models import InitializationOptions 4 | import mcp.types as types 5 | from mcp.server import NotificationOptions, Server 6 | from pydantic import AnyUrl 7 | import mcp.server.stdio 8 | 9 | # Store notes as a simple key-value dict to demonstrate state management 10 | notes: dict[str, str] = {} 11 | 12 | server = Server("mcp-server-on-raspi") 13 | 14 | @server.list_resources() 15 | async def handle_list_resources() -> list[types.Resource]: 16 | """ 17 | List available note resources. 18 | Each note is exposed as a resource with a custom note:// URI scheme. 19 | """ 20 | return [ 21 | types.Resource( 22 | uri=AnyUrl(f"note://internal/{name}"), 23 | name=f"Note: {name}", 24 | description=f"A simple note named {name}", 25 | mimeType="text/plain", 26 | ) 27 | for name in notes 28 | ] 29 | 30 | @server.read_resource() 31 | async def handle_read_resource(uri: AnyUrl) -> str: 32 | """ 33 | Read a specific note's content by its URI. 34 | The note name is extracted from the URI host component. 35 | """ 36 | if uri.scheme != "note": 37 | raise ValueError(f"Unsupported URI scheme: {uri.scheme}") 38 | 39 | name = uri.path 40 | if name is not None: 41 | name = name.lstrip("/") 42 | return notes[name] 43 | raise ValueError(f"Note not found: {name}") 44 | 45 | @server.list_prompts() 46 | async def handle_list_prompts() -> list[types.Prompt]: 47 | """ 48 | List available prompts. 49 | Each prompt can have optional arguments to customize its behavior. 50 | """ 51 | return [ 52 | types.Prompt( 53 | name="summarize-notes", 54 | description="Creates a summary of all notes", 55 | arguments=[ 56 | types.PromptArgument( 57 | name="style", 58 | description="Style of the summary (brief/detailed)", 59 | required=False, 60 | ) 61 | ], 62 | ) 63 | ] 64 | 65 | @server.get_prompt() 66 | async def handle_get_prompt( 67 | name: str, arguments: dict[str, str] | None 68 | ) -> types.GetPromptResult: 69 | """ 70 | Generate a prompt by combining arguments with server state. 71 | The prompt includes all current notes and can be customized via arguments. 72 | """ 73 | if name != "summarize-notes": 74 | raise ValueError(f"Unknown prompt: {name}") 75 | 76 | style = (arguments or {}).get("style", "brief") 77 | detail_prompt = " Give extensive details." if style == "detailed" else "" 78 | 79 | return types.GetPromptResult( 80 | description="Summarize the current notes", 81 | messages=[ 82 | types.PromptMessage( 83 | role="user", 84 | content=types.TextContent( 85 | type="text", 86 | text=f"Here are the current notes to summarize:{detail_prompt}\n\n" 87 | + "\n".join( 88 | f"- {name}: {content}" 89 | for name, content in notes.items() 90 | ), 91 | ), 92 | ) 93 | ], 94 | ) 95 | 96 | @server.list_tools() 97 | async def handle_list_tools() -> list[types.Tool]: 98 | """ 99 | List available tools. 100 | Each tool specifies its arguments using JSON Schema validation. 101 | """ 102 | return [ 103 | types.Tool( 104 | name="add-note", 105 | description="Add a new note", 106 | inputSchema={ 107 | "type": "object", 108 | "properties": { 109 | "name": {"type": "string"}, 110 | "content": {"type": "string"}, 111 | }, 112 | "required": ["name", "content"], 113 | }, 114 | ) 115 | ] 116 | 117 | @server.call_tool() 118 | async def handle_call_tool( 119 | name: str, arguments: dict | None 120 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 121 | """ 122 | Handle tool execution requests. 123 | Tools can modify server state and notify clients of changes. 124 | """ 125 | if name != "add-note": 126 | raise ValueError(f"Unknown tool: {name}") 127 | 128 | if not arguments: 129 | raise ValueError("Missing arguments") 130 | 131 | note_name = arguments.get("name") 132 | content = arguments.get("content") 133 | 134 | if not note_name or not content: 135 | raise ValueError("Missing name or content") 136 | 137 | # Update server state 138 | notes[note_name] = content 139 | 140 | # Notify clients that resources have changed 141 | await server.request_context.session.send_resource_list_changed() 142 | 143 | return [ 144 | types.TextContent( 145 | type="text", 146 | text=f"Added note '{note_name}' with content: {content}", 147 | ) 148 | ] 149 | 150 | async def main(): 151 | # Run the server using stdin/stdout streams 152 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 153 | await server.run( 154 | read_stream, 155 | write_stream, 156 | InitializationOptions( 157 | server_name="mcp-server-on-raspi", 158 | server_version="0.1.0", 159 | capabilities=server.get_capabilities( 160 | notification_options=NotificationOptions(), 161 | experimental_capabilities={}, 162 | ), 163 | ), 164 | ) ```