Skip to main content
Glama

FluidSynth MCP Server

by kimjune01
test_server.py31.2 kB
import unittest from pathlib import Path import json import tempfile import shutil import sys import asyncio from server import MidiCompositionServer, ProjectState, TrackState import pytest import os from midi_server import MidiCompositionServer, FLUIDSYNTH_AVAILABLE from soundfont_manager import SoundfontManager, FLUIDSYNTH_AVAILABLE import httpx import mido class TestMidiCompositionServer(unittest.TestCase): def setUp(self): print("Setting up test...", file=sys.stderr) # Create a temporary directory for testing self.test_dir = Path(tempfile.mkdtemp()) print(f"Created temp dir: {self.test_dir}", file=sys.stderr) self.server = MidiCompositionServer() print("Created server instance", file=sys.stderr) self.server.workspace_dir = self.test_dir / "workspace" self.server.workspace_dir.mkdir(exist_ok=True) print(f"Created workspace dir: {self.server.workspace_dir}", file=sys.stderr) print("Test setup complete", file=sys.stderr) def tearDown(self): print("Tearing down test...", file=sys.stderr) try: if hasattr(self, "test_dir"): print(f"Removing temp dir: {self.test_dir}", file=sys.stderr) shutil.rmtree(self.test_dir) print("Test teardown complete", file=sys.stderr) except Exception as e: print(f"Error in teardown: {e}", file=sys.stderr) async def async_test_project_creation(self): """Test creating a new project""" try: print("\nStarting project creation test...", file=sys.stderr) project_name = "test_project" tempo = 120 time_signature = (4, 4) # Create project print("Creating project...", file=sys.stderr) result = self.server.create_project(project_name, tempo, time_signature) print(f"Project creation result: {result}", file=sys.stderr) self.assertTrue(result, "Project creation failed") # Verify project was created print("Verifying project...", file=sys.stderr) self.assertIsNotNone(self.server.current_project) self.assertEqual(self.server.current_project.name, project_name) self.assertEqual(self.server.current_project.tempo, tempo) self.assertEqual(self.server.current_project.time_signature, time_signature) # Verify project file was created print("Checking project file...", file=sys.stderr) project_file = self.server.workspace_dir / f"{project_name}.json" self.assertTrue(project_file.exists()) # Verify file contents print("Reading project file...", file=sys.stderr) with open(project_file) as f: data = json.load(f) self.assertEqual(data["name"], project_name) self.assertEqual(data["tempo"], tempo) self.assertEqual(tuple(data["time_signature"]), time_signature) print("Project creation test complete", file=sys.stderr) except Exception as e: print(f"Error in test: {e}", file=sys.stderr) raise def test_project_creation(self): """Run the async test in an event loop""" asyncio.run(self.async_test_project_creation()) async def async_test_debug_project_file(self): """Test the debug function for project files""" try: print("\nStarting debug function test...", file=sys.stderr) # Test with non-existent project print("Testing non-existent project...", file=sys.stderr) debug_info = self.server.debug_project_file("nonexistent") self.assertFalse(debug_info["file_exists"]) self.assertFalse(debug_info["current_project"]) self.assertFalse(debug_info["in_projects_dict"]) self.assertIsNone(debug_info["file_contents"]) print("Non-existent project test passed", file=sys.stderr) # Create a project and test debug info print("Creating test project...", file=sys.stderr) project_name = "debug_test" self.server.create_project(project_name, 120, (4, 4)) print("Getting debug info for created project...", file=sys.stderr) debug_info = self.server.debug_project_file(project_name) # Verify debug info self.assertTrue(debug_info["file_exists"]) self.assertTrue(debug_info["current_project"]) self.assertTrue(debug_info["in_projects_dict"]) self.assertGreater(debug_info["file_size"], 0) self.assertIsNotNone(debug_info["file_contents"]) # Verify file contents contents = debug_info["file_contents"] self.assertEqual(contents["name"], project_name) self.assertEqual(contents["tempo"], 120) self.assertEqual(tuple(contents["time_signature"]), (4, 4)) print("Debug function test complete", file=sys.stderr) except Exception as e: print(f"Error in debug test: {e}", file=sys.stderr) raise def test_debug_project_file(self): """Run the async debug test in an event loop""" asyncio.run(self.async_test_debug_project_file()) def test_track_management(self): """Test track creation and management""" # Create a project first assert self.server.create_project("test_project", 120, (4, 4))["success"] # Create a track track_name = "piano" instrument = "piano" channel = 0 result = self.server.create_track(track_name, instrument, channel) assert result["success"] # Verify track was created self.assertIn(track_name, self.server.current_project.tracks) track = self.server.current_project.tracks[track_name] self.assertEqual(track.name, track_name) self.assertEqual(track.instrument, instrument) self.assertEqual(track.channel, channel) # Test track muting assert self.server.mute_track(track_name)["success"] self.assertTrue(self.server.current_project.tracks[track_name].is_muted) # Test track solo assert self.server.solo_track(track_name)["success"] self.assertTrue(self.server.current_project.tracks[track_name].is_solo) # Test track volume volume = 0.8 assert self.server.set_track_volume(track_name, volume)["success"] self.assertEqual(self.server.current_project.tracks[track_name].volume, volume) # Test track pan pan = 0.5 assert self.server.set_track_pan(track_name, pan)["success"] self.assertEqual(self.server.current_project.tracks[track_name].pan, pan) def test_project_persistence(self): """Test saving and loading projects""" # Create and save a project project_name = "persistence_test" assert self.server.create_project(project_name, 120, (4, 4))["success"] assert self.server.create_track("piano", "piano", 0)["success"] # Save the project assert self.server.save_project()["success"] # Create a new server instance to test loading new_server = MidiCompositionServer() new_server.workspace_dir = self.server.workspace_dir # Load the project assert new_server.load_project(project_name)["success"] # Verify loaded state self.assertEqual(new_server.current_project.name, project_name) self.assertEqual(new_server.current_project.tempo, 120) self.assertEqual(new_server.current_project.time_signature, (4, 4)) self.assertIn("piano", new_server.current_project.tracks) def test_synth_settings(self): """Test FluidSynth settings management""" # Skip test if FluidSynth is not available if not FLUIDSYNTH_AVAILABLE: pytest.skip("FluidSynth is not available") # Test gain setting gain = 1.0 # Default gain value self.server.soundfont_manager.fs.setting("synth.gain", gain) self.assertEqual( self.server.soundfont_manager.fs.get_setting("synth.gain"), gain ) # Test reverb settings reverb_params = {"room_size": 0.5, "damping": 0.2, "width": 0.7, "level": 0.8} self.server.soundfont_manager.set_reverb(**reverb_params) self.assertEqual( self.server.soundfont_manager.fs.get_reverb_roomsize(), reverb_params["room_size"], ) self.assertEqual( self.server.soundfont_manager.fs.get_reverb_damp(), reverb_params["damping"] ) self.assertEqual( self.server.soundfont_manager.fs.get_reverb_width(), reverb_params["width"] ) self.assertEqual( self.server.soundfont_manager.fs.get_reverb_level(), reverb_params["level"] ) # Test chorus settings chorus_params = {"nr": 4, "level": 0.8, "speed": 0.4, "depth": 10.0, "type": 0} self.server.soundfont_manager.set_chorus(**chorus_params) self.assertEqual( self.server.soundfont_manager.fs.get_chorus_nr(), chorus_params["nr"] ) self.assertEqual( self.server.soundfont_manager.fs.get_chorus_level(), chorus_params["level"] ) self.assertEqual( self.server.soundfont_manager.fs.get_chorus_speed(), chorus_params["speed"] ) self.assertEqual( self.server.soundfont_manager.fs.get_chorus_depth(), chorus_params["depth"] ) self.assertEqual( self.server.soundfont_manager.fs.get_chorus_type(), chorus_params["type"] ) @pytest.fixture def soundfont_manager(): """Create a fresh soundfont manager for each test.""" manager = SoundfontManager() yield manager manager.cleanup() @pytest.fixture def server(): """Create a fresh server instance for each test.""" server = MidiCompositionServer() # Clean up workspace directory before each test if server.workspace_dir.exists(): for file in server.workspace_dir.glob("*.json"): file.unlink() yield server # Cleanup after tests if server.current_project: server.remove_project(server.current_project.name) server.cleanup() def test_create_project(server): """Test project creation functionality.""" assert server.create_project("test_project", 120, (4, 4)) assert server.current_project is not None assert server.current_project.name == "test_project" assert server.current_project.tempo == 120 assert server.current_project.time_signature == (4, 4) def test_create_track(server): """Test track creation functionality.""" assert server.create_project("test_project", 120, (4, 4))["success"] result = server.create_track("track1", "piano", 0) assert result["success"] assert "track1" in server.current_project.tracks track = server.current_project.tracks["track1"] assert track.instrument == "piano" assert track.channel == 0 def test_mute_track(server): server.create_project("test_project", 120, (4, 4)) assert server.create_track("track1", "piano", 0)["success"] result = server.mute_track("track1") assert result["success"] assert server.current_project.tracks["track1"].is_muted def test_solo_track(server): server.create_project("test_project", 120, (4, 4)) assert server.create_track("track1", "piano", 0)["success"] result = server.solo_track("track1") assert result["success"] assert server.current_project.tracks["track1"].is_solo def test_set_track_volume(server): server.create_project("test_project", 120, (4, 4)) assert server.create_track("track1", "piano", 0)["success"] result = server.set_track_volume("track1", 0.5) assert result["success"] assert server.current_project.tracks["track1"].volume == 0.5 def test_set_track_pan(server): server.create_project("test_project", 120, (4, 4)) assert server.create_track("track1", "piano", 0)["success"] result = server.set_track_pan("track1", -0.5) assert result["success"] assert server.current_project.tracks["track1"].pan == -0.5 def test_save_and_load_project(server): # Create and save a project assert server.create_project("test_project", 120, (4, 4))["success"] assert server.create_track("track1", "piano", 0)["success"] assert server.set_track_volume("track1", 0.5)["success"] assert server.save_project()["success"] # Create a new server instance and load the project new_server = MidiCompositionServer() assert new_server.load_project("test_project")["success"] assert new_server.current_project is not None assert new_server.current_project.name == "test_project" assert "track1" in new_server.current_project.tracks assert new_server.current_project.tracks["track1"].volume == 0.5 def test_export_midi(server, tmp_path): server.create_project("test_project", 120, (4, 4)) server.create_track("track1", "piano", 0) midi_path = tmp_path / "test.mid" assert server.export_midi(str(midi_path)) assert midi_path.exists() def test_export_audio(server, tmp_path): server.create_project("test_project", 120, (4, 4)) server.create_track("track1", "piano", 0) audio_path = tmp_path / "test.wav" assert server.export_audio(str(audio_path), "wav") assert audio_path.exists() def test_play_audio_file(server, tmp_path): # Create a test audio file audio_path = tmp_path / "test.wav" # Create a simple WAV file with 1 second of silence import wave import struct with wave.open(str(audio_path), "w") as wav_file: # Set parameters nchannels = 1 sampwidth = 2 framerate = 44100 nframes = framerate comptype = "NONE" compname = "not compressed" wav_file.setparams( (nchannels, sampwidth, framerate, nframes, comptype, compname) ) # Write 1 second of silence for _ in range(framerate): value = struct.pack("h", 0) wav_file.writeframes(value) # Test playing the audio file result = server.play_audio_file(str(audio_path)) assert result["success"] # Test playing non-existent file result = server.play_audio_file("nonexistent.wav") assert not result["success"] assert "error" in result assert "File not found" in result["error"] def test_play_audio_file_mcp(server, tmp_path): """Test the MCP play_audio_file function.""" # Create a test audio file audio_path = tmp_path / "test.wav" # Create a simple WAV file with 1 second of silence import wave import struct with wave.open(str(audio_path), "w") as wav_file: # Set parameters nchannels = 1 sampwidth = 2 framerate = 44100 nframes = framerate comptype = "NONE" compname = "not compressed" wav_file.setparams( (nchannels, sampwidth, framerate, nframes, comptype, compname) ) # Write 1 second of silence for _ in range(framerate): value = struct.pack("h", 0) wav_file.writeframes(value) # Test playing the audio file result = server.play_audio_file(str(audio_path)) assert result["success"] # Test playing non-existent file result = server.play_audio_file("nonexistent.wav") assert not result["success"] assert "error" in result assert "File not found" in result["error"] def test_remove_project(server): # Create a test project server.create_project("test_project", 120, (4, 4)) server.create_track("track1", "piano", 0) server.save_project() # Test removing the project result = server.remove_project("test_project") assert result["success"] assert not server.workspace_dir.joinpath("test_project.json").exists() assert server.current_project is None # Test removing non-existent project result = server.remove_project("non_existent_project") assert not result["success"] assert "error" in result def test_inspect_projects(server): # Create a test project assert server.create_project("test_project", 120, (4, 4))["success"] assert server.create_track("track1", "piano", 0)["success"] assert server.save_project()["success"] # Test inspecting projects result = server.inspect_projects() assert result["success"] assert "data" in result assert "projects" in result["data"] assert "test_project" in result["data"]["projects"] assert ( result["data"]["projects"]["test_project"]["tracks"]["track1"]["instrument"] == "piano" ) def test_add_notes(server): """Test adding multiple notes to a track.""" # Create a project and track assert server.create_project("test_project", 120, (4, 4))["success"] assert server.create_track("track1", "piano", 0)["success"] # Test adding valid notes notes = [ {"note": 60, "velocity": 100, "time": 0}, # Middle C {"note": 64, "velocity": 100, "time": 480}, # E {"note": 67, "velocity": 100, "time": 960}, # G ] result = server.add_notes("track1", notes) assert result["success"] # Verify notes were added track = server.current_project.tracks["track1"] assert len(track.events) == 3 assert track.events[0]["note"] == 60 assert track.events[1]["note"] == 64 assert track.events[2]["note"] == 67 # Test invalid note values invalid_notes = [{"note": 128, "velocity": 100}] # Note > 127 result = server.add_notes("track1", invalid_notes) assert not result["success"] assert "must be between 0 and 127" in result["error"] # Test invalid velocity values invalid_notes = [{"note": 60, "velocity": 128}] # Velocity > 127 result = server.add_notes("track1", invalid_notes) assert not result["success"] assert "must be between 0 and 127" in result["error"] # Test missing required fields invalid_notes = [{"note": 60}] # Missing velocity result = server.add_notes("track1", invalid_notes) assert not result["success"] assert "must have 'note' and 'velocity' fields" in result["error"] # Test non-existent track result = server.add_notes("nonexistent_track", notes) assert not result["success"] assert "not found" in result["error"] def test_add_notes_mcp(server): """Test the MCP add_notes function.""" # Create a project and track assert server.create_project("test_project", 120, (4, 4))["success"] assert server.create_track("track1", "piano", 0)["success"] assert server.save_project()["success"] # Test adding valid notes notes = [ {"note": 60, "velocity": 100, "time": 0}, {"note": 64, "velocity": 100, "time": 480}, {"note": 67, "velocity": 100, "time": 960}, ] result = server.add_notes("track1", notes) assert result["success"] # Test non-existent project result = server.add_notes("nonexistent_track", notes) assert not result["success"] assert "not found" in result["error"] def test_soundfont_management(soundfont_manager, tmp_path): """Test soundfont management functionality.""" # Test with non-existent file result = soundfont_manager.add_soundfont("nonexistent.sf2") assert not result["success"] assert "not found" in result["error"] # Test with non-sf2 file invalid_path = tmp_path / "test.txt" with open(invalid_path, "w") as f: f.write("not a soundfont") result = soundfont_manager.add_soundfont(str(invalid_path)) assert not result["success"] assert "must be a .sf2 soundfont file" in result["error"] # Test with invalid sf2 file invalid_sf2 = tmp_path / "test.sf2" with open(invalid_sf2, "w") as f: f.write("invalid sf2 data") result = soundfont_manager.add_soundfont(str(invalid_sf2)) if FLUIDSYNTH_AVAILABLE: assert not result["success"] assert "Failed to load soundfont" in result["error"] else: assert not result["success"] assert "FluidSynth is not available" in result["error"] # Test listing soundfonts (should be empty) result = soundfont_manager.list_soundfonts() if FLUIDSYNTH_AVAILABLE: assert result["success"] assert "soundfonts" in result["data"] assert len(result["data"]["soundfonts"]) == 0 else: assert not result["success"] assert "FluidSynth is not available" in result["error"] # Test removing non-existent soundfont result = soundfont_manager.remove_soundfont(999) assert not result["success"] assert "not found" in result["error"] def test_soundfont_management_mcp(server, tmp_path): """Test the MCP soundfont management functions.""" # Test with non-existent file result = server.add_soundfont("nonexistent.sf2") assert not result["success"] assert "not found" in result["error"] # Test with invalid sf2 file invalid_sf2 = tmp_path / "test.sf2" with open(invalid_sf2, "w") as f: f.write("invalid sf2 data") result = server.add_soundfont(str(invalid_sf2)) if FLUIDSYNTH_AVAILABLE: assert not result["success"] assert "Failed to load soundfont" in result["error"] else: assert not result["success"] assert "FluidSynth is not available" in result["error"] # Test listing soundfonts (should be empty) result = server.list_soundfonts() if FLUIDSYNTH_AVAILABLE: assert result["success"] assert "soundfonts" in result["data"] assert len(result["data"]["soundfonts"]) == 0 else: assert not result["success"] assert "FluidSynth is not available" in result["error"] def test_remove_soundfont_mcp(server): """Test removing a soundfont using MCP.""" result = server.remove_soundfont(1) assert not result["success"] assert "not found" in result["error"].lower() def test_find_soundfont(server): """Test searching for soundfonts without downloading.""" # Test finding an existing soundfont result = server.find_soundfont("Piano") assert result["success"] assert len(result["data"]["matches"]) > 0 for match in result["data"]["matches"]: assert "name" in match assert "url" in match assert "category" in match # Test searching for non-existent soundfont result = server.find_soundfont("nonexistent") assert not result["success"] assert "no soundfont found matching" in result["error"].lower() def test_download_soundfont(server, monkeypatch): """Test downloading a soundfont.""" # Mock the find_soundfont method to return a known soundfont def mock_find_soundfont(name): return { "success": True, "data": { "matches": [ { "name": "Piano", "category": "Piano", "url": "http://example.com/piano.sf2", } ] }, } monkeypatch.setattr(server.soundfont_manager, "find_soundfont", mock_find_soundfont) # Mock the httpx.get function class MockResponse: status_code = 200 content = b"mock soundfont data" def mock_get(url): return MockResponse() monkeypatch.setattr(httpx, "get", mock_get) # Test downloading a known soundfont result = server.download_soundfont("Piano") assert result["success"] assert "Piano" in result["data"]["name"] assert result["data"]["category"] == "Piano" assert os.path.exists(result["data"]["filepath"]) # Clean up downloaded file if result["success"]: os.remove(result["data"]["filepath"]) def test_create_track_with_soundfont(server, monkeypatch): """Test creating a track with automatic soundfont handling.""" # Create a project first assert server.create_project("TestProject", 120, (4, 4))["success"] # Mock the find_soundfont and download_soundfont methods def mock_find_soundfont(name): return { "success": True, "data": { "matches": [ { "name": name, "category": name, "url": f"http://example.com/{name}.sf2", } ] }, } def mock_download_soundfont(name): return { "success": True, "data": { "name": name, "category": name, "filepath": f"soundfonts/{name}.sf2", }, } monkeypatch.setattr(server.soundfont_manager, "find_soundfont", mock_find_soundfont) monkeypatch.setattr( server.soundfont_manager, "download_soundfont", mock_download_soundfont ) # Test creating a track with a piano (program 0) result = server.create_track("Piano Track", "piano", 0) assert result["success"] assert "Piano Track" in server.current_project.tracks assert server.current_project.tracks["Piano Track"].instrument == "piano" def test_create_track_invalid_instrument(server): """Test creating a track with an invalid instrument name.""" # Create a project first assert server.create_project("TestProject", 120, (4, 4))["success"] # Try to create a track with an invalid instrument name result = server.create_track("Invalid Track", "invalid", 0) assert not result["success"] assert ( "Could not find or download soundfont for instrument invalid" in result["error"] ) def test_create_track_missing_soundfont(server, monkeypatch): """Test creating a track when soundfont can't be found or downloaded.""" # Create a project first assert server.create_project("TestProject", 120, (4, 4))["success"] # Mock the find_soundfont and download_soundfont methods to simulate failure def mock_find_soundfont(name): return {"success": False, "error": "Soundfont not found"} def mock_download_soundfont(name): return {"success": False, "error": "Download failed"} monkeypatch.setattr(server.soundfont_manager, "find_soundfont", mock_find_soundfont) monkeypatch.setattr( server.soundfont_manager, "download_soundfont", mock_download_soundfont ) # Try to create a track with a piano (program 0) result = server.create_track("Piano Track", "piano", 0) assert not result["success"] assert "Could not find or download soundfont" in result["error"] assert "Piano Track" not in server.current_project.tracks def test_create_track_no_project(server): """Test creating a track when no project is loaded.""" result = server.create_track("Test Track", "piano", 0) assert not result["success"] assert "No project is currently loaded" in result["error"] def test_create_track_existing_soundfont(server, monkeypatch): """Test creating a track when soundfont is already available.""" # Create a project first assert server.create_project("TestProject", 120, (4, 4))["success"] # Mock the find_soundfont method to simulate existing soundfont def mock_find_soundfont(name): return { "success": True, "data": { "matches": [ { "name": name, "category": name, "url": f"http://example.com/{name}.sf2", } ] }, } monkeypatch.setattr(server.soundfont_manager, "find_soundfont", mock_find_soundfont) # Create a track with a piano (program 0) result = server.create_track("Piano Track", "piano", 0) assert result["success"] assert "Piano Track" in server.current_project.tracks assert server.current_project.tracks["Piano Track"].instrument == "piano" def test_export_midi_with_strings(server, tmp_path): """Test exporting MIDI with string instruments.""" server.create_project("test_project", 120, (4, 4)) server.create_track("strings", "strings", 0) # Add some notes to the string track notes = [ {"type": "note", "note": 60, "velocity": 64, "time": 0, "duration": 480}, {"type": "note", "note": 64, "velocity": 64, "time": 480, "duration": 480}, {"type": "note", "note": 67, "velocity": 64, "time": 960, "duration": 480}, ] server.add_notes("strings", notes) midi_path = tmp_path / "test_strings.mid" result = server.export_midi(str(midi_path)) assert result["success"] assert midi_path.exists() # Read back the MIDI file and verify program number midi_file = mido.MidiFile(str(midi_path)) assert len(midi_file.tracks) == 1 # Find program change message program_msgs = [msg for msg in midi_file.tracks[0] if msg.type == "program_change"] assert len(program_msgs) == 1 assert program_msgs[0].program == 48 # String Ensemble 1 # Verify notes note_on_msgs = [ msg for msg in midi_file.tracks[0] if msg.type == "note_on" and msg.velocity > 0 ] assert len(note_on_msgs) == 3 assert note_on_msgs[0].note == 60 # C4 assert note_on_msgs[1].note == 64 # E4 assert note_on_msgs[2].note == 67 # G4 def test_get_midi_settings(server): """Test getting MIDI settings.""" result = server.get_midi_settings() assert result["success"] assert "data" in result assert result["data"]["ticks_per_beat"] == 480 assert result["data"]["max_note_value"] == 127 assert result["data"]["max_velocity"] == 127 assert result["data"]["max_channel"] == 15 assert result["data"]["drum_channel"] == 9 def test_export_midi_simultaneous_notes(server, tmp_path): """Test exporting MIDI with simultaneous notes.""" server.create_project("test_project", 120, (4, 4)) server.create_track("piano", "piano", 0) # Add three simultaneous notes (C major chord) notes = [ {"note": 60, "velocity": 80, "time": 0, "duration": 480}, # C4 {"note": 64, "velocity": 80, "time": 0, "duration": 480}, # E4 {"note": 67, "velocity": 80, "time": 0, "duration": 480}, # G4 ] server.add_notes("piano", notes) midi_path = tmp_path / "test_simultaneous.mid" result = server.export_midi(str(midi_path)) assert result["success"] assert midi_path.exists() # Read back the MIDI file and verify notes midi_file = mido.MidiFile(str(midi_path)) assert len(midi_file.tracks) == 1 # Get all note_on messages note_on_msgs = [ msg for msg in midi_file.tracks[0] if msg.type == "note_on" and msg.velocity > 0 ] assert len(note_on_msgs) == 3 # Verify that all notes start at time 0 for msg in note_on_msgs: assert msg.time == 0 # Verify the notes are C4, E4, G4 notes = sorted(msg.note for msg in note_on_msgs) assert notes == [60, 64, 67] # C4, E4, G4 if __name__ == "__main__": pytest.main([__file__])

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kimjune01/synth-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server