judith.ibanez commited on
Commit
bec2d56
Β·
1 Parent(s): 3697654
Files changed (5) hide show
  1. Dockerfile +15 -4
  2. claude_desktop_config.json +9 -0
  3. requirements.txt +2 -1
  4. simple_mcp_server.py +200 -0
  5. start.sh +13 -0
Dockerfile CHANGED
@@ -30,16 +30,27 @@ RUN apt-get install --fix-missing -y /tmp/audiveris-fixed.deb || apt-get install
30
 
31
  RUN rm Audiveris-5.6.1-ubuntu24.04-x86_64.deb
32
 
33
- ## Install Gradio MCP
34
  RUN apt-get install --fix-missing -y python3 python3-pip || apt-get install -y -f
35
  # TODO: use a python virtual environment
36
- RUN pip install --break-system-packages gradio[mcp]
37
 
38
  ## Clean
39
  RUN apt-get clean
40
 
41
- ## Copy MCP server and execute it
42
  COPY app.py /app/app.py
 
 
 
 
43
  WORKDIR /app
44
 
45
- CMD ["python3", "app.py"]
 
 
 
 
 
 
 
 
30
 
31
  RUN rm Audiveris-5.6.1-ubuntu24.04-x86_64.deb
32
 
33
+ ## Install Python and dependencies
34
  RUN apt-get install --fix-missing -y python3 python3-pip || apt-get install -y -f
35
  # TODO: use a python virtual environment
36
+ RUN pip install --break-system-packages gradio[mcp] mcp
37
 
38
  ## Clean
39
  RUN apt-get clean
40
 
41
+ ## Copy application files
42
  COPY app.py /app/app.py
43
+ COPY simple_mcp_server.py /app/simple_mcp_server.py
44
+ COPY claude_desktop_config.json /app/claude_desktop_config.json
45
+ COPY requirements.txt /app/requirements.txt
46
+ COPY start.sh /app/start.sh
47
  WORKDIR /app
48
 
49
+ # Create output directory for processed files
50
+ RUN mkdir -p /tmp/output
51
+
52
+ # Make start script executable
53
+ RUN chmod +x start.sh
54
+
55
+ # Default to Gradio interface, but allow MCP server with argument
56
+ CMD ["./start.sh"]
claude_desktop_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "audiveris-music-recognition": {
4
+ "command": "python3",
5
+ "args": ["simple_mcp_server.py"],
6
+ "cwd": "/app"
7
+ }
8
+ }
9
+ }
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  gradio>=4.0.0
2
  requests
3
- typing-extensions
 
 
1
  gradio>=4.0.0
2
  requests
3
+ typing-extensions
4
+ mcp
simple_mcp_server.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple MCP Server for Music Recognition
4
+ Based on patterns from https://llmindset.co.uk/posts/2025/01/mcp-files-resources-part1/
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import tempfile
12
+ from typing import Any, List
13
+ from mcp.server import Server
14
+ from mcp.server.models import InitializationOptions
15
+ import mcp.server.stdio
16
+ import mcp.types as types
17
+
18
+ # Initialize the MCP server
19
+ server = Server("audiveris-music-recognition")
20
+
21
+ def process_music_score(file_path: str) -> dict:
22
+ """Process a music score PDF using Audiveris"""
23
+ audiveris = "/opt/audiveris/bin/Audiveris"
24
+ output_dir = "/tmp/output"
25
+
26
+ # Ensure output directory exists
27
+ os.makedirs(output_dir, exist_ok=True)
28
+
29
+ file_name = os.path.basename(file_path)
30
+ musicxml_name = os.path.splitext(file_name)[0] + ".mxl"
31
+ output_file = os.path.join(output_dir, musicxml_name)
32
+
33
+ cmd = [audiveris, "-batch", "-export", "-output", output_dir, file_path]
34
+
35
+ try:
36
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
37
+
38
+ if result.returncode == 0 and os.path.exists(output_file):
39
+ # Read the MusicXML content (limit size as per MCP spec)
40
+ with open(output_file, 'r', encoding='utf-8') as f:
41
+ content = f.read()
42
+ # Limit to ~800KB to stay under 1MB limit
43
+ if len(content.encode('utf-8')) > 800000:
44
+ content = content[:800000] + "\n... [Content truncated due to size limit]"
45
+
46
+ return {
47
+ "success": True,
48
+ "output_file": output_file,
49
+ "musicxml_content": content,
50
+ "message": f"Successfully converted {file_name} to MusicXML"
51
+ }
52
+ else:
53
+ return {
54
+ "success": False,
55
+ "error": f"Audiveris failed: {result.stderr}",
56
+ "message": f"Failed to convert {file_name}"
57
+ }
58
+ except subprocess.TimeoutExpired:
59
+ return {
60
+ "success": False,
61
+ "error": "Processing timeout",
62
+ "message": f"Conversion of {file_name} timed out"
63
+ }
64
+ except Exception as e:
65
+ return {
66
+ "success": False,
67
+ "error": str(e),
68
+ "message": f"Error processing {file_name}"
69
+ }
70
+
71
+ @server.list_resources()
72
+ async def handle_list_resources() -> List[types.Resource]:
73
+ """List available resources - in this case, processed music files"""
74
+ resources = []
75
+ output_dir = "/tmp/output"
76
+
77
+ if os.path.exists(output_dir):
78
+ for file in os.listdir(output_dir):
79
+ if file.endswith('.mxl'):
80
+ file_path = os.path.join(output_dir, file)
81
+ resources.append(types.Resource(
82
+ uri=f"file://{file_path}",
83
+ name=f"MusicXML: {file}",
84
+ description=f"Processed music score: {file}",
85
+ mimeType="application/vnd.recordare.musicxml"
86
+ ))
87
+
88
+ return resources
89
+
90
+ @server.read_resource()
91
+ async def handle_read_resource(uri: str) -> str:
92
+ """Read a resource (MusicXML file)"""
93
+ if not uri.startswith("file://"):
94
+ raise ValueError(f"Unsupported URI scheme: {uri}")
95
+
96
+ file_path = uri[7:] # Remove "file://" prefix
97
+
98
+ if not os.path.exists(file_path):
99
+ raise FileNotFoundError(f"File not found: {file_path}")
100
+
101
+ with open(file_path, 'r', encoding='utf-8') as f:
102
+ content = f.read()
103
+ # Ensure we stay under 1MB limit
104
+ if len(content.encode('utf-8')) > 1000000:
105
+ content = content[:900000] + "\n... [Content truncated due to size limit]"
106
+ return content
107
+
108
+ @server.list_tools()
109
+ async def handle_list_tools() -> List[types.Tool]:
110
+ """List available tools"""
111
+ return [
112
+ types.Tool(
113
+ name="convert_music_score",
114
+ description="Convert a PDF music score to MusicXML format using Audiveris OCR",
115
+ inputSchema={
116
+ "type": "object",
117
+ "properties": {
118
+ "file_path": {
119
+ "type": "string",
120
+ "description": "Path to the PDF file to convert"
121
+ }
122
+ },
123
+ "required": ["file_path"]
124
+ }
125
+ ),
126
+ types.Tool(
127
+ name="list_processed_scores",
128
+ description="List all processed music scores available as resources",
129
+ inputSchema={
130
+ "type": "object",
131
+ "properties": {},
132
+ "additionalProperties": False
133
+ }
134
+ )
135
+ ]
136
+
137
+ @server.call_tool()
138
+ async def handle_call_tool(name: str, arguments: dict[str, Any] | None) -> List[types.TextContent]:
139
+ """Handle tool calls"""
140
+
141
+ if name == "convert_music_score":
142
+ if not arguments or "file_path" not in arguments:
143
+ raise ValueError("Missing required argument: file_path")
144
+
145
+ file_path = arguments["file_path"]
146
+
147
+ if not os.path.exists(file_path):
148
+ return [types.TextContent(
149
+ type="text",
150
+ text=f"Error: File not found: {file_path}"
151
+ )]
152
+
153
+ result = process_music_score(file_path)
154
+
155
+ if result["success"]:
156
+ # Return a summary, not the full content (that's available as a resource)
157
+ response_text = f"""βœ… {result['message']}
158
+
159
+ πŸ“ Output file: {result['output_file']}
160
+ πŸ“„ MusicXML file is now available as a resource
161
+
162
+ The converted MusicXML content is available through the MCP resources. You can access it by listing resources and reading the specific MusicXML file."""
163
+ else:
164
+ response_text = f"❌ {result['message']}\n\nError: {result['error']}"
165
+
166
+ return [types.TextContent(type="text", text=response_text)]
167
+
168
+ elif name == "list_processed_scores":
169
+ resources = await handle_list_resources()
170
+
171
+ if not resources:
172
+ response_text = "No processed music scores found."
173
+ else:
174
+ response_text = "πŸ“š Available processed music scores:\n\n"
175
+ for resource in resources:
176
+ response_text += f"β€’ {resource.name}\n URI: {resource.uri}\n Description: {resource.description}\n\n"
177
+
178
+ return [types.TextContent(type="text", text=response_text)]
179
+
180
+ else:
181
+ raise ValueError(f"Unknown tool: {name}")
182
+
183
+ async def main():
184
+ """Run the MCP server"""
185
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
186
+ await server.run(
187
+ read_stream,
188
+ write_stream,
189
+ InitializationOptions(
190
+ server_name="audiveris-music-recognition",
191
+ server_version="0.1.0",
192
+ capabilities=server.get_capabilities(
193
+ notification_options=None,
194
+ experimental_capabilities=None,
195
+ ),
196
+ ),
197
+ )
198
+
199
+ if __name__ == "__main__":
200
+ asyncio.run(main())
start.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Create output directory
4
+ mkdir -p /tmp/output
5
+
6
+ # Check if we should run MCP server or Gradio interface
7
+ if [ "$1" = "mcp" ]; then
8
+ echo "Starting MCP Server..."
9
+ python3 simple_mcp_server.py
10
+ else
11
+ echo "Starting Gradio Interface..."
12
+ python3 app.py
13
+ fi