feat(mcp): align language catalog with detector

Expose all languages already recognized by lang_detect in lang_list, and keep the two paths synchronized through a shared catalog.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 10:09:04 +00:00
parent 116df41200
commit a541c95dc8
2 changed files with 141 additions and 69 deletions

View file

@ -438,7 +438,7 @@ type GetSupportedLanguagesInput struct{}
// GetSupportedLanguagesOutput contains the list of supported languages.
//
// // len(out.Languages) == 15
// // len(out.Languages) == 23
// // out.Languages[0].ID == "typescript"
type GetSupportedLanguagesOutput struct {
Languages []LanguageInfo `json:"languages"` // all recognised languages
@ -568,24 +568,7 @@ func (s *Service) detectLanguage(ctx context.Context, req *mcp.CallToolRequest,
}
func (s *Service) getSupportedLanguages(ctx context.Context, req *mcp.CallToolRequest, input GetSupportedLanguagesInput) (*mcp.CallToolResult, GetSupportedLanguagesOutput, error) {
languages := []LanguageInfo{
{ID: "typescript", Name: "TypeScript", Extensions: []string{".ts", ".tsx"}},
{ID: "javascript", Name: "JavaScript", Extensions: []string{".js", ".jsx"}},
{ID: "go", Name: "Go", Extensions: []string{".go"}},
{ID: "python", Name: "Python", Extensions: []string{".py"}},
{ID: "rust", Name: "Rust", Extensions: []string{".rs"}},
{ID: "java", Name: "Java", Extensions: []string{".java"}},
{ID: "php", Name: "PHP", Extensions: []string{".php"}},
{ID: "ruby", Name: "Ruby", Extensions: []string{".rb"}},
{ID: "html", Name: "HTML", Extensions: []string{".html", ".htm"}},
{ID: "css", Name: "CSS", Extensions: []string{".css"}},
{ID: "json", Name: "JSON", Extensions: []string{".json"}},
{ID: "yaml", Name: "YAML", Extensions: []string{".yaml", ".yml"}},
{ID: "markdown", Name: "Markdown", Extensions: []string{".md", ".markdown"}},
{ID: "sql", Name: "SQL", Extensions: []string{".sql"}},
{ID: "shell", Name: "Shell", Extensions: []string{".sh", ".bash"}},
}
return nil, GetSupportedLanguagesOutput{Languages: languages}, nil
return nil, GetSupportedLanguagesOutput{Languages: supportedLanguages()}, nil
}
func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input EditDiffInput) (*mcp.CallToolResult, EditDiffOutput, error) {
@ -627,57 +610,78 @@ func (s *Service) editDiff(ctx context.Context, req *mcp.CallToolRequest, input
// detectLanguageFromPath maps file extensions to language IDs.
func detectLanguageFromPath(path string) string {
if core.PathBase(path) == "Dockerfile" {
return "dockerfile"
}
ext := core.PathExt(path)
switch ext {
case ".ts", ".tsx":
return "typescript"
case ".js", ".jsx":
return "javascript"
case ".go":
return "go"
case ".py":
return "python"
case ".rs":
return "rust"
case ".rb":
return "ruby"
case ".java":
return "java"
case ".php":
return "php"
case ".c", ".h":
return "c"
case ".cpp", ".hpp", ".cc", ".cxx":
return "cpp"
case ".cs":
return "csharp"
case ".html", ".htm":
return "html"
case ".css":
return "css"
case ".scss":
return "scss"
case ".json":
return "json"
case ".yaml", ".yml":
return "yaml"
case ".xml":
return "xml"
case ".md", ".markdown":
return "markdown"
case ".sql":
return "sql"
case ".sh", ".bash":
return "shell"
case ".swift":
return "swift"
case ".kt", ".kts":
return "kotlin"
default:
if core.PathBase(path) == "Dockerfile" {
return "dockerfile"
}
return "plaintext"
if lang, ok := languageByExtension[ext]; ok {
return lang
}
return "plaintext"
}
var languageByExtension = map[string]string{
".ts": "typescript",
".tsx": "typescript",
".js": "javascript",
".jsx": "javascript",
".go": "go",
".py": "python",
".rs": "rust",
".rb": "ruby",
".java": "java",
".php": "php",
".c": "c",
".h": "c",
".cpp": "cpp",
".hpp": "cpp",
".cc": "cpp",
".cxx": "cpp",
".cs": "csharp",
".html": "html",
".htm": "html",
".css": "css",
".scss": "scss",
".json": "json",
".yaml": "yaml",
".yml": "yaml",
".xml": "xml",
".md": "markdown",
".markdown": "markdown",
".sql": "sql",
".sh": "shell",
".bash": "shell",
".swift": "swift",
".kt": "kotlin",
".kts": "kotlin",
}
func supportedLanguages() []LanguageInfo {
return []LanguageInfo{
{ID: "typescript", Name: "TypeScript", Extensions: []string{".ts", ".tsx"}},
{ID: "javascript", Name: "JavaScript", Extensions: []string{".js", ".jsx"}},
{ID: "go", Name: "Go", Extensions: []string{".go"}},
{ID: "python", Name: "Python", Extensions: []string{".py"}},
{ID: "rust", Name: "Rust", Extensions: []string{".rs"}},
{ID: "ruby", Name: "Ruby", Extensions: []string{".rb"}},
{ID: "java", Name: "Java", Extensions: []string{".java"}},
{ID: "php", Name: "PHP", Extensions: []string{".php"}},
{ID: "c", Name: "C", Extensions: []string{".c", ".h"}},
{ID: "cpp", Name: "C++", Extensions: []string{".cpp", ".hpp", ".cc", ".cxx"}},
{ID: "csharp", Name: "C#", Extensions: []string{".cs"}},
{ID: "html", Name: "HTML", Extensions: []string{".html", ".htm"}},
{ID: "css", Name: "CSS", Extensions: []string{".css"}},
{ID: "scss", Name: "SCSS", Extensions: []string{".scss"}},
{ID: "json", Name: "JSON", Extensions: []string{".json"}},
{ID: "yaml", Name: "YAML", Extensions: []string{".yaml", ".yml"}},
{ID: "xml", Name: "XML", Extensions: []string{".xml"}},
{ID: "markdown", Name: "Markdown", Extensions: []string{".md", ".markdown"}},
{ID: "sql", Name: "SQL", Extensions: []string{".sql"}},
{ID: "shell", Name: "Shell", Extensions: []string{".sh", ".bash"}},
{ID: "swift", Name: "Swift", Extensions: []string{".swift"}},
{ID: "kotlin", Name: "Kotlin", Extensions: []string{".kt", ".kts"}},
{ID: "dockerfile", Name: "Dockerfile", Extensions: []string{}},
}
}

View file

@ -95,6 +95,74 @@ func TestNew_Good_RegistersBuiltInTools(t *testing.T) {
}
}
func TestGetSupportedLanguages_Good_IncludesAllDetectedLanguages(t *testing.T) {
s, err := New(Options{})
if err != nil {
t.Fatalf("Failed to create service: %v", err)
}
_, out, err := s.getSupportedLanguages(nil, nil, GetSupportedLanguagesInput{})
if err != nil {
t.Fatalf("getSupportedLanguages failed: %v", err)
}
if got, want := len(out.Languages), 23; got != want {
t.Fatalf("expected %d supported languages, got %d", want, got)
}
got := map[string]bool{}
for _, lang := range out.Languages {
got[lang.ID] = true
}
for _, want := range []string{
"typescript",
"javascript",
"go",
"python",
"rust",
"ruby",
"java",
"php",
"c",
"cpp",
"csharp",
"html",
"css",
"scss",
"json",
"yaml",
"xml",
"markdown",
"sql",
"shell",
"swift",
"kotlin",
"dockerfile",
} {
if !got[want] {
t.Fatalf("expected language %q to be listed", want)
}
}
}
func TestDetectLanguageFromPath_Good_KnownExtensions(t *testing.T) {
cases := map[string]string{
"main.go": "go",
"index.tsx": "typescript",
"style.scss": "scss",
"Program.cs": "csharp",
"module.kt": "kotlin",
"docker/Dockerfile": "dockerfile",
}
for path, want := range cases {
if got := detectLanguageFromPath(path); got != want {
t.Fatalf("detectLanguageFromPath(%q) = %q, want %q", path, got, want)
}
}
}
func TestMedium_Good_ReadWrite(t *testing.T) {
tmpDir := t.TempDir()
s, err := New(Options{WorkspaceRoot: tmpDir})