Commit 14f1b12
Changed files (8)
cmd
cmd/git/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func main() {
+ server := NewGitServer()
+
+ // Set up basic initialization
+ server.SetInitializeHandler(func(req mcp.InitializeRequest) (mcp.InitializeResult, error) {
+ // Use default initialization
+ return mcp.InitializeResult{}, nil
+ })
+
+ ctx := context.Background()
+ if err := server.Run(ctx); err != nil {
+ log.Fatalf("Server error: %v", err)
+ }
+}
\ No newline at end of file
cmd/git/server.go
@@ -0,0 +1,530 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+// GitServer implements the Git MCP server
+type GitServer struct {
+ *mcp.Server
+}
+
+// NewGitServer creates a new Git MCP server
+func NewGitServer() *GitServer {
+ server := mcp.NewServer("mcp-git", "1.0.0")
+
+ gitServer := &GitServer{
+ Server: server,
+ }
+
+ // Register all git tools
+ gitServer.registerTools()
+
+ return gitServer
+}
+
+// registerTools registers all Git tools with the server
+func (gs *GitServer) registerTools() {
+ gs.RegisterTool("git_status", gs.HandleGitStatus)
+ gs.RegisterTool("git_diff_unstaged", gs.HandleGitDiffUnstaged)
+ gs.RegisterTool("git_diff_staged", gs.HandleGitDiffStaged)
+ gs.RegisterTool("git_diff", gs.HandleGitDiff)
+ gs.RegisterTool("git_commit", gs.HandleGitCommit)
+ gs.RegisterTool("git_add", gs.HandleGitAdd)
+ gs.RegisterTool("git_reset", gs.HandleGitReset)
+ gs.RegisterTool("git_log", gs.HandleGitLog)
+ gs.RegisterTool("git_create_branch", gs.HandleGitCreateBranch)
+ gs.RegisterTool("git_checkout", gs.HandleGitCheckout)
+ gs.RegisterTool("git_show", gs.HandleGitShow)
+ gs.RegisterTool("git_init", gs.HandleGitInit)
+}
+
+// ListTools returns all available Git tools
+func (gs *GitServer) ListTools() []mcp.Tool {
+ return []mcp.Tool{
+ {
+ Name: "git_status",
+ Description: "Shows the working tree status",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ },
+ },
+ {
+ Name: "git_diff_unstaged",
+ Description: "Shows changes in the working directory that are not yet staged",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ },
+ },
+ {
+ Name: "git_diff_staged",
+ Description: "Shows changes that are staged for commit",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ },
+ },
+ {
+ Name: "git_diff",
+ Description: "Shows differences between branches or commits",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "target": map[string]interface{}{
+ "type": "string",
+ "description": "Target branch or commit to diff against",
+ },
+ },
+ "required": []string{"repo_path", "target"},
+ },
+ },
+ {
+ Name: "git_commit",
+ Description: "Records changes to the repository",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "message": map[string]interface{}{
+ "type": "string",
+ "description": "Commit message",
+ },
+ },
+ "required": []string{"repo_path", "message"},
+ },
+ },
+ {
+ Name: "git_add",
+ Description: "Adds file contents to the staging area",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "files": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
+ },
+ "description": "List of files to add",
+ },
+ },
+ "required": []string{"repo_path", "files"},
+ },
+ },
+ {
+ Name: "git_reset",
+ Description: "Unstages all staged changes",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ },
+ },
+ {
+ Name: "git_log",
+ Description: "Shows the commit logs",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "max_count": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of commits to show",
+ "default": 10,
+ },
+ },
+ "required": []string{"repo_path"},
+ },
+ },
+ {
+ Name: "git_create_branch",
+ Description: "Creates a new branch from an optional base branch",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "branch_name": map[string]interface{}{
+ "type": "string",
+ "description": "Name of the new branch",
+ },
+ "base_branch": map[string]interface{}{
+ "type": "string",
+ "description": "Base branch to create from (optional)",
+ },
+ },
+ "required": []string{"repo_path", "branch_name"},
+ },
+ },
+ {
+ Name: "git_checkout",
+ Description: "Switches branches",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "branch_name": map[string]interface{}{
+ "type": "string",
+ "description": "Name of the branch to checkout",
+ },
+ },
+ "required": []string{"repo_path", "branch_name"},
+ },
+ },
+ {
+ Name: "git_show",
+ Description: "Shows the contents of a commit",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "revision": map[string]interface{}{
+ "type": "string",
+ "description": "Commit hash or reference to show",
+ },
+ },
+ "required": []string{"repo_path", "revision"},
+ },
+ },
+ {
+ Name: "git_init",
+ Description: "Initialize a new Git repository",
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path where to initialize the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ },
+ },
+ }
+}
+
+// Tool handlers
+
+func (gs *GitServer) HandleGitStatus(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ output, err := gs.runGitCommand(repoPath, "status")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git status failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Repository status:\n%s", output))), nil
+}
+
+func (gs *GitServer) HandleGitDiffUnstaged(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ output, err := gs.runGitCommand(repoPath, "diff")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Unstaged changes:\n%s", output))), nil
+}
+
+func (gs *GitServer) HandleGitDiffStaged(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ output, err := gs.runGitCommand(repoPath, "diff", "--cached")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git diff --cached failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Staged changes:\n%s", output))), nil
+}
+
+func (gs *GitServer) HandleGitDiff(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ target, ok := req.Arguments["target"].(string)
+ if !ok {
+ return mcp.NewToolError("target is required"), nil
+ }
+
+ output, err := gs.runGitCommand(repoPath, "diff", target)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Diff with %s:\n%s", target, output))), nil
+}
+
+func (gs *GitServer) HandleGitCommit(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ message, ok := req.Arguments["message"].(string)
+ if !ok {
+ return mcp.NewToolError("message is required"), nil
+ }
+
+ output, err := gs.runGitCommand(repoPath, "commit", "-m", message)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git commit failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Changes committed successfully:\n%s", output))), nil
+}
+
+func (gs *GitServer) HandleGitAdd(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ filesInterface, ok := req.Arguments["files"]
+ if !ok {
+ return mcp.NewToolError("files is required"), nil
+ }
+
+ files, err := gs.convertToStringSlice(filesInterface)
+ if err != nil {
+ return mcp.NewToolError("files must be an array of strings"), nil
+ }
+
+ args := append([]string{"add"}, files...)
+ _, err = gs.runGitCommand(repoPath, args...)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git add failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent("Files staged successfully")), nil
+}
+
+func (gs *GitServer) HandleGitReset(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ _, err := gs.runGitCommand(repoPath, "reset")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git reset failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent("All staged changes reset")), nil
+}
+
+func (gs *GitServer) HandleGitLog(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ maxCount := 10
+ if mc, exists := req.Arguments["max_count"]; exists {
+ if count, ok := mc.(float64); ok {
+ maxCount = int(count)
+ }
+ }
+
+ output, err := gs.runGitCommand(repoPath, "log", "--oneline", "-n", strconv.Itoa(maxCount))
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git log failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Commit history:\n%s", output))), nil
+}
+
+func (gs *GitServer) HandleGitCreateBranch(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ branchName, ok := req.Arguments["branch_name"].(string)
+ if !ok {
+ return mcp.NewToolError("branch_name is required"), nil
+ }
+
+ baseBranch, _ := req.Arguments["base_branch"].(string)
+
+ var args []string
+ if baseBranch != "" {
+ args = []string{"checkout", "-b", branchName, baseBranch}
+ } else {
+ args = []string{"checkout", "-b", branchName}
+ }
+
+ _, err := gs.runGitCommand(repoPath, args...)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git create branch failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Created branch '%s'", branchName))), nil
+}
+
+func (gs *GitServer) HandleGitCheckout(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ branchName, ok := req.Arguments["branch_name"].(string)
+ if !ok {
+ return mcp.NewToolError("branch_name is required"), nil
+ }
+
+ _, err := gs.runGitCommand(repoPath, "checkout", branchName)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git checkout failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Switched to branch '%s'", branchName))), nil
+}
+
+func (gs *GitServer) HandleGitShow(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ revision, ok := req.Arguments["revision"].(string)
+ if !ok {
+ return mcp.NewToolError("revision is required"), nil
+ }
+
+ output, err := gs.runGitCommand(repoPath, "show", revision)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git show failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(output)), nil
+}
+
+func (gs *GitServer) HandleGitInit(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ return mcp.NewToolError("repo_path is required"), nil
+ }
+
+ // Ensure directory exists
+ if err := os.MkdirAll(repoPath, 0755); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("failed to create directory: %v", err)), nil
+ }
+
+ _, err := gs.runGitCommand(repoPath, "init")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git init failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Initialized empty Git repository in %s", repoPath))), nil
+}
+
+// Helper methods
+
+func (gs *GitServer) runGitCommand(repoPath string, args ...string) (string, error) {
+ // Check if path exists
+ if _, err := os.Stat(repoPath); os.IsNotExist(err) {
+ return "", fmt.Errorf("repository path does not exist: %s", repoPath)
+ }
+
+ // Check if it's a git repository (except for init command)
+ if len(args) > 0 && args[0] != "init" {
+ gitDir := filepath.Join(repoPath, ".git")
+ if _, err := os.Stat(gitDir); os.IsNotExist(err) {
+ return "", fmt.Errorf("not a git repository: %s", repoPath)
+ }
+ }
+
+ cmd := exec.Command("git", args...)
+ cmd.Dir = repoPath
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("%v: %s", err, string(output))
+ }
+
+ return strings.TrimSpace(string(output)), nil
+}
+
+func (gs *GitServer) convertToStringSlice(input interface{}) ([]string, error) {
+ switch v := input.(type) {
+ case []interface{}:
+ result := make([]string, len(v))
+ for i, item := range v {
+ str, ok := item.(string)
+ if !ok {
+ return nil, fmt.Errorf("item at index %d is not a string", i)
+ }
+ result[i] = str
+ }
+ return result, nil
+ case []string:
+ return v, nil
+ default:
+ return nil, fmt.Errorf("input is not a slice")
+ }
+}
\ No newline at end of file
cmd/git/server_test.go
@@ -0,0 +1,215 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/xlgmokha/mcp/pkg/mcp"
+)
+
+func TestGitServer_GitStatus(t *testing.T) {
+ // Setup test repo
+ tempDir, err := os.MkdirTemp("", "git-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ // Initialize git repo
+ if err := initGitRepo(tempDir); err != nil {
+ t.Fatal(err)
+ }
+
+ server := NewGitServer()
+
+ // Test git status
+ req := mcp.CallToolRequest{
+ Name: "git_status",
+ Arguments: map[string]interface{}{
+ "repo_path": tempDir,
+ },
+ }
+
+ result, err := server.HandleGitStatus(req)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ if len(result.Content) == 0 {
+ t.Fatal("Expected content in result")
+ }
+
+ textContent, ok := result.Content[0].(mcp.TextContent)
+ if !ok {
+ t.Fatal("Expected TextContent")
+ }
+
+ if textContent.Text == "" {
+ t.Fatal("Expected non-empty status text")
+ }
+
+ // Should contain status information
+ if len(textContent.Text) < 10 {
+ t.Fatal("Expected meaningful status output")
+ }
+}
+
+func TestGitServer_GitInit(t *testing.T) {
+ // Setup temp directory
+ tempDir, err := os.MkdirTemp("", "git-init-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ server := NewGitServer()
+
+ // Test git init
+ req := mcp.CallToolRequest{
+ Name: "git_init",
+ Arguments: map[string]interface{}{
+ "repo_path": tempDir,
+ },
+ }
+
+ result, err := server.HandleGitInit(req)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ if len(result.Content) == 0 {
+ t.Fatal("Expected content in result")
+ }
+
+ textContent, ok := result.Content[0].(mcp.TextContent)
+ if !ok {
+ t.Fatal("Expected TextContent")
+ }
+
+ // Check that .git directory was created
+ gitDir := filepath.Join(tempDir, ".git")
+ if _, err := os.Stat(gitDir); os.IsNotExist(err) {
+ t.Fatal("Expected .git directory to be created")
+ }
+
+ // Check response message
+ if !contains(textContent.Text, "Initialized") {
+ t.Fatalf("Expected init success message, got: %s", textContent.Text)
+ }
+}
+
+func TestGitServer_GitAdd(t *testing.T) {
+ // Setup test repo
+ tempDir, err := os.MkdirTemp("", "git-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ // Initialize git repo
+ if err := initGitRepo(tempDir); err != nil {
+ t.Fatal(err)
+ }
+
+ // Create a test file
+ testFile := filepath.Join(tempDir, "test.txt")
+ if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ server := NewGitServer()
+
+ // Test git add
+ req := mcp.CallToolRequest{
+ Name: "git_add",
+ Arguments: map[string]interface{}{
+ "repo_path": tempDir,
+ "files": []interface{}{"test.txt"},
+ },
+ }
+
+ result, err := server.HandleGitAdd(req)
+ if err != nil {
+ t.Fatalf("Expected no error, got %v", err)
+ }
+
+ if len(result.Content) == 0 {
+ t.Fatal("Expected content in result")
+ }
+
+ textContent, ok := result.Content[0].(mcp.TextContent)
+ if !ok {
+ t.Fatal("Expected TextContent")
+ }
+
+ // Check response message
+ if !contains(textContent.Text, "staged") {
+ t.Fatalf("Expected staged success message, got: %s", textContent.Text)
+ }
+}
+
+func TestGitServer_ListTools(t *testing.T) {
+ server := NewGitServer()
+ tools := server.ListTools()
+
+ expectedTools := []string{
+ "git_status", "git_diff_unstaged", "git_diff_staged", "git_diff",
+ "git_commit", "git_add", "git_reset", "git_log", "git_create_branch",
+ "git_checkout", "git_show", "git_init",
+ }
+
+ if len(tools) != len(expectedTools) {
+ t.Fatalf("Expected %d tools, got %d", len(expectedTools), len(tools))
+ }
+
+ toolNames := make(map[string]bool)
+ for _, tool := range tools {
+ toolNames[tool.Name] = true
+ }
+
+ for _, expected := range expectedTools {
+ if !toolNames[expected] {
+ t.Fatalf("Expected tool %s not found", expected)
+ }
+ }
+}
+
+// Helper functions
+func initGitRepo(dir string) error {
+ // Use actual git init command for testing
+ server := NewGitServer()
+ req := mcp.CallToolRequest{
+ Name: "git_init",
+ Arguments: map[string]interface{}{
+ "repo_path": dir,
+ },
+ }
+
+ result, err := server.HandleGitInit(req)
+ if err != nil {
+ return err
+ }
+
+ if result.IsError {
+ return fmt.Errorf("git init failed")
+ }
+
+ return nil
+}
+
+func contains(s, substr string) bool {
+ return len(s) > 0 && len(substr) > 0 &&
+ (s == substr || (len(s) >= len(substr) &&
+ findSubstring(s, substr)))
+}
+
+func findSubstring(s, substr string) bool {
+ for i := 0; i <= len(s)-len(substr); i++ {
+ if s[i:i+len(substr)] == substr {
+ return true
+ }
+ }
+ return false
+}
\ No newline at end of file
docs/mcp-server-specifications.md
@@ -0,0 +1,831 @@
+# MCP Server Feature Specifications for Go Implementation
+
+This document provides detailed specifications for implementing MCP servers in Go based on analysis of the Python reference implementations. Each server must implement the Model Context Protocol using JSON-RPC 2.0 over stdio.
+
+## Core MCP Protocol Requirements
+
+### Base Protocol
+- **Transport**: JSON-RPC 2.0 over stdio (stdin/stdout)
+- **Connection**: Stateful client-server connection
+- **Initialization**: Capability negotiation during handshake
+- **Message Format**: All messages must follow JSON-RPC 2.0 specification
+
+### Required MCP Methods
+All servers must implement these base protocol methods:
+- `initialize` - Capability negotiation and server info
+- `initialized` - Notification that initialization is complete
+- `shutdown` - Graceful shutdown request
+- `exit` - Immediate termination notification
+
+### Server Capabilities
+Servers can implement any combination of:
+- **Tools** - Functions the AI model can execute
+- **Resources** - Context and data access
+- **Prompts** - Templated messages and workflows
+
+## 1. Git Server (`mcp-server-git`)
+
+### Dependencies
+- Git CLI or go-git library
+- File system access for repository operations
+
+### Tools
+
+#### `git_status`
+- **Description**: Shows the working tree status
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"}
+ },
+ "required": ["repo_path"]
+ }
+ ```
+- **Implementation**: Execute `git status` or use go-git Status()
+- **Output**: TextContent with repository status
+
+#### `git_diff_unstaged`
+- **Description**: Shows changes in working directory not yet staged
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"}
+ },
+ "required": ["repo_path"]
+ }
+ ```
+- **Implementation**: Execute `git diff` or use go-git diff functionality
+- **Output**: TextContent with unstaged changes
+
+#### `git_diff_staged`
+- **Description**: Shows changes that are staged for commit
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"}
+ },
+ "required": ["repo_path"]
+ }
+ ```
+- **Implementation**: Execute `git diff --cached`
+- **Output**: TextContent with staged changes
+
+#### `git_diff`
+- **Description**: Shows differences between branches or commits
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "target": {"type": "string", "description": "Target branch or commit to compare with"}
+ },
+ "required": ["repo_path", "target"]
+ }
+ ```
+- **Implementation**: Execute `git diff <target>`
+- **Output**: TextContent with diff output
+
+#### `git_commit`
+- **Description**: Records changes to the repository
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "message": {"type": "string", "description": "Commit message"}
+ },
+ "required": ["repo_path", "message"]
+ }
+ ```
+- **Implementation**: Execute `git commit -m "message"`
+- **Output**: TextContent with commit confirmation and hash
+
+#### `git_add`
+- **Description**: Adds file contents to the staging area
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "files": {"type": "array", "items": {"type": "string"}, "description": "Array of file paths to stage"}
+ },
+ "required": ["repo_path", "files"]
+ }
+ ```
+- **Implementation**: Execute `git add <files...>`
+- **Output**: TextContent with staging confirmation
+
+#### `git_reset`
+- **Description**: Unstages all staged changes
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"}
+ },
+ "required": ["repo_path"]
+ }
+ ```
+- **Implementation**: Execute `git reset`
+- **Output**: TextContent with reset confirmation
+
+#### `git_log`
+- **Description**: Shows the commit logs
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "max_count": {"type": "integer", "description": "Maximum number of commits to show", "default": 10}
+ },
+ "required": ["repo_path"]
+ }
+ ```
+- **Implementation**: Execute `git log --max-count=<n>` or use go-git log iterator
+- **Output**: TextContent with formatted commit history
+
+#### `git_create_branch`
+- **Description**: Creates a new branch from an optional base branch
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "branch_name": {"type": "string", "description": "Name of the new branch"},
+ "base_branch": {"type": "string", "description": "Starting point for the new branch", "default": null}
+ },
+ "required": ["repo_path", "branch_name"]
+ }
+ ```
+- **Implementation**: Execute `git checkout -b <branch_name> [base_branch]`
+- **Output**: TextContent with branch creation confirmation
+
+#### `git_checkout`
+- **Description**: Switches branches
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "branch_name": {"type": "string", "description": "Name of branch to checkout"}
+ },
+ "required": ["repo_path", "branch_name"]
+ }
+ ```
+- **Implementation**: Execute `git checkout <branch_name>`
+- **Output**: TextContent with checkout confirmation
+
+#### `git_show`
+- **Description**: Shows the contents of a commit
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to Git repository"},
+ "revision": {"type": "string", "description": "The revision (commit hash, branch name, tag) to show"}
+ },
+ "required": ["repo_path", "revision"]
+ }
+ ```
+- **Implementation**: Execute `git show <revision>` or use go-git commit details
+- **Output**: TextContent with commit details and diff
+
+#### `git_init`
+- **Description**: Initialize a new Git repository
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "repo_path": {"type": "string", "description": "Path to directory to initialize git repo"}
+ },
+ "required": ["repo_path"]
+ }
+ ```
+- **Implementation**: Execute `git init` or use go-git PlainInit()
+- **Output**: TextContent with initialization confirmation
+
+### Configuration
+- Command line argument: `--repository <path>` to specify default repository
+- Can also detect repositories from MCP roots capability
+
+## 2. Filesystem Server (`mcp-server-filesystem`)
+
+### Dependencies
+- File system access with configurable permissions
+- Pattern matching for file search
+- Text diff generation
+
+### Security Model
+- **Sandboxing**: Only allow operations within specified directories
+- **Validation**: All paths must be validated against allowed directories
+- **Error Handling**: Graceful handling of permission errors
+
+### Tools
+
+#### `read_file`
+- **Description**: Read complete contents of a file
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "Absolute path to the file to read"}
+ },
+ "required": ["path"]
+ }
+ ```
+- **Implementation**: Read file with UTF-8 encoding, handle various encodings
+- **Output**: TextContent with file contents
+- **Security**: Validate path is within allowed directories
+
+#### `read_multiple_files`
+- **Description**: Read multiple files simultaneously
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "paths": {"type": "array", "items": {"type": "string"}, "description": "Array of file paths to read"}
+ },
+ "required": ["paths"]
+ }
+ ```
+- **Implementation**: Read multiple files, continue on individual failures
+- **Output**: TextContent with results for each file
+- **Security**: Validate all paths are within allowed directories
+
+#### `write_file`
+- **Description**: Create new file or overwrite existing
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "File location"},
+ "content": {"type": "string", "description": "File content"}
+ },
+ "required": ["path", "content"]
+ }
+ ```
+- **Implementation**: Write content to file with UTF-8 encoding
+- **Output**: TextContent with write confirmation
+- **Security**: Validate path is within allowed directories
+
+#### `edit_file`
+- **Description**: Make selective edits using pattern matching
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "File to edit"},
+ "edits": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "oldText": {"type": "string", "description": "Text to search for"},
+ "newText": {"type": "string", "description": "Text to replace with"}
+ },
+ "required": ["oldText", "newText"]
+ }
+ },
+ "dryRun": {"type": "boolean", "description": "Preview changes without applying", "default": false}
+ },
+ "required": ["path", "edits"]
+ }
+ ```
+- **Implementation**:
+ - Line-based and multi-line content matching
+ - Whitespace normalization with indentation preservation
+ - Multiple simultaneous edits with correct positioning
+ - Git-style diff output
+- **Output**: TextContent with diff information or edit confirmation
+- **Security**: Validate path is within allowed directories
+
+#### `create_directory`
+- **Description**: Create new directory or ensure it exists
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "Directory path to create"}
+ },
+ "required": ["path"]
+ }
+ ```
+- **Implementation**: Create directory and parent directories if needed
+- **Output**: TextContent with creation confirmation
+- **Security**: Validate path is within allowed directories
+
+#### `list_directory`
+- **Description**: List directory contents with type indicators
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "Directory path to list"}
+ },
+ "required": ["path"]
+ }
+ ```
+- **Implementation**: List files and directories with [FILE] or [DIR] prefixes
+- **Output**: TextContent with directory listing
+- **Security**: Validate path is within allowed directories
+
+#### `move_file`
+- **Description**: Move or rename files and directories
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "source": {"type": "string", "description": "Source path"},
+ "destination": {"type": "string", "description": "Destination path"}
+ },
+ "required": ["source", "destination"]
+ }
+ ```
+- **Implementation**: Move/rename operation, fail if destination exists
+- **Output**: TextContent with move confirmation
+- **Security**: Validate both paths are within allowed directories
+
+#### `search_files`
+- **Description**: Recursively search for files/directories
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "Starting directory"},
+ "pattern": {"type": "string", "description": "Search pattern"},
+ "excludePatterns": {"type": "array", "items": {"type": "string"}, "description": "Exclude patterns", "default": []}
+ },
+ "required": ["path", "pattern"]
+ }
+ ```
+- **Implementation**: Case-insensitive recursive search with glob patterns
+- **Output**: TextContent with matching file paths
+- **Security**: Validate path is within allowed directories
+
+#### `get_file_info`
+- **Description**: Get detailed file/directory metadata
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "path": {"type": "string", "description": "File/directory path"}
+ },
+ "required": ["path"]
+ }
+ ```
+- **Implementation**: Return size, timestamps, permissions, type
+- **Output**: TextContent with file metadata
+- **Security**: Validate path is within allowed directories
+
+#### `list_allowed_directories`
+- **Description**: List all directories the server is allowed to access
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {},
+ "required": []
+ }
+ ```
+- **Implementation**: Return configured allowed directories
+- **Output**: TextContent with allowed directory list
+
+### Configuration
+- Command line arguments: List of allowed directories
+- Example: `mcp-server-filesystem /home/user/documents /home/user/projects`
+
+## 3. Fetch Server (`mcp-server-fetch`)
+
+### Dependencies
+- HTTP client for web requests
+- HTML to Markdown conversion
+- robots.txt parsing
+- URL validation and parsing
+
+### Security Considerations
+- **robots.txt Compliance**: Check robots.txt before autonomous fetching
+- **User Agent**: Different user agents for autonomous vs manual requests
+- **Proxy Support**: Optional proxy configuration
+- **Content Filtering**: Handle various content types appropriately
+
+### Tools
+
+#### `fetch`
+- **Description**: Fetches a URL from the internet and extracts its contents as markdown
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "url": {"type": "string", "format": "uri", "description": "URL to fetch"},
+ "max_length": {"type": "integer", "description": "Maximum number of characters to return", "default": 5000, "minimum": 1, "maximum": 999999},
+ "start_index": {"type": "integer", "description": "Start content from this character index", "default": 0, "minimum": 0},
+ "raw": {"type": "boolean", "description": "Get raw content without markdown conversion", "default": false}
+ },
+ "required": ["url"]
+ }
+ ```
+- **Implementation**:
+ - Check robots.txt compliance for autonomous requests
+ - Fetch URL with appropriate user agent
+ - Convert HTML to markdown unless raw requested
+ - Handle content truncation and pagination
+ - Support content type detection
+- **Output**: TextContent with fetched content
+- **Error Handling**: Detailed error messages for robots.txt violations, HTTP errors
+
+### Prompts
+
+#### `fetch`
+- **Description**: Fetch a URL and extract its contents as markdown
+- **Arguments**:
+ ```json
+ [
+ {
+ "name": "url",
+ "description": "URL to fetch",
+ "required": true
+ }
+ ]
+ ```
+- **Implementation**: Manual fetch without robots.txt restrictions
+
+### Configuration Options
+- `--custom-user-agent`: Custom User-Agent string
+- `--ignore-robots-txt`: Ignore robots.txt restrictions
+- `--proxy-url`: Proxy URL for requests
+
+## 4. Memory Server (`mcp-server-memory`)
+
+### Dependencies
+- JSON file storage for persistence
+- Graph data structure for entities and relations
+- Text search capabilities
+
+### Data Model
+
+#### Entity
+```json
+{
+ "name": "string (unique identifier)",
+ "entityType": "string (e.g., 'person', 'organization', 'event')",
+ "observations": ["array of strings"]
+}
+```
+
+#### Relation
+```json
+{
+ "from": "string (source entity name)",
+ "to": "string (target entity name)",
+ "relationType": "string (relationship type in active voice)"
+}
+```
+
+### Tools
+
+#### `create_entities`
+- **Description**: Create multiple new entities in the knowledge graph
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "entities": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "Entity identifier"},
+ "entityType": {"type": "string", "description": "Type classification"},
+ "observations": {"type": "array", "items": {"type": "string"}, "description": "Associated observations"}
+ },
+ "required": ["name", "entityType", "observations"]
+ }
+ }
+ },
+ "required": ["entities"]
+ }
+ ```
+- **Implementation**: Add entities to graph, ignore duplicates by name
+- **Output**: TextContent with creation results
+
+#### `create_relations`
+- **Description**: Create multiple new relations between entities
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "relations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "from": {"type": "string", "description": "Source entity name"},
+ "to": {"type": "string", "description": "Target entity name"},
+ "relationType": {"type": "string", "description": "Relationship type"}
+ },
+ "required": ["from", "to", "relationType"]
+ }
+ }
+ },
+ "required": ["relations"]
+ }
+ ```
+- **Implementation**: Add relations to graph, skip duplicates
+- **Output**: TextContent with creation results
+
+#### `add_observations`
+- **Description**: Add new observations to existing entities
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "observations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "entityName": {"type": "string", "description": "Target entity"},
+ "contents": {"type": "array", "items": {"type": "string"}, "description": "New observations to add"}
+ },
+ "required": ["entityName", "contents"]
+ }
+ }
+ },
+ "required": ["observations"]
+ }
+ ```
+- **Implementation**: Add observations to existing entities
+- **Output**: TextContent with added observations per entity
+- **Error**: Fail if entity doesn't exist
+
+#### `delete_entities`
+- **Description**: Remove entities and their relations
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "entityNames": {"type": "array", "items": {"type": "string"}, "description": "Entity names to delete"}
+ },
+ "required": ["entityNames"]
+ }
+ ```
+- **Implementation**: Remove entities and cascade delete relations
+- **Output**: TextContent with deletion results
+
+#### `delete_observations`
+- **Description**: Remove specific observations from entities
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "deletions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "entityName": {"type": "string", "description": "Target entity"},
+ "observations": {"type": "array", "items": {"type": "string"}, "description": "Observations to remove"}
+ },
+ "required": ["entityName", "observations"]
+ }
+ }
+ },
+ "required": ["deletions"]
+ }
+ ```
+- **Implementation**: Remove specific observations from entities
+- **Output**: TextContent with deletion results
+
+#### `delete_relations`
+- **Description**: Remove specific relations from the graph
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "relations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "from": {"type": "string", "description": "Source entity name"},
+ "to": {"type": "string", "description": "Target entity name"},
+ "relationType": {"type": "string", "description": "Relationship type"}
+ },
+ "required": ["from", "to", "relationType"]
+ }
+ }
+ },
+ "required": ["relations"]
+ }
+ ```
+- **Implementation**: Remove specific relations
+- **Output**: TextContent with deletion results
+
+#### `read_graph`
+- **Description**: Read the entire knowledge graph
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {},
+ "required": []
+ }
+ ```
+- **Implementation**: Return complete graph structure
+- **Output**: TextContent with full graph data
+
+#### `search_nodes`
+- **Description**: Search for nodes based on query
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "Search query"}
+ },
+ "required": ["query"]
+ }
+ ```
+- **Implementation**: Search entity names, types, and observation content
+- **Output**: TextContent with matching entities and relations
+
+#### `open_nodes`
+- **Description**: Retrieve specific nodes by name
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "names": {"type": "array", "items": {"type": "string"}, "description": "Entity names to retrieve"}
+ },
+ "required": ["names"]
+ }
+ ```
+- **Implementation**: Return requested entities and their relations
+- **Output**: TextContent with requested nodes
+
+### Configuration
+- Environment variable: `MEMORY_FILE_PATH` for custom storage location
+- Default: `memory.json` in server directory
+
+## 5. Sequential Thinking Server (`mcp-server-sequential-thinking`)
+
+### Dependencies
+- State management for thought sequences
+- Dynamic thought process tracking
+
+### Tools
+
+#### `sequentialthinking`
+- **Description**: Dynamic and reflective problem-solving through thought sequences
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "thought": {"type": "string", "description": "Your current thinking step"},
+ "nextThoughtNeeded": {"type": "boolean", "description": "Whether another thought step is needed"},
+ "thoughtNumber": {"type": "integer", "description": "Current thought number", "minimum": 1},
+ "totalThoughts": {"type": "integer", "description": "Estimated total thoughts needed", "minimum": 1},
+ "isRevision": {"type": "boolean", "description": "Whether this revises previous thinking"},
+ "revisesThought": {"type": "integer", "description": "Which thought is being reconsidered", "minimum": 1},
+ "branchFromThought": {"type": "integer", "description": "Branching point thought number", "minimum": 1},
+ "branchId": {"type": "string", "description": "Branch identifier"},
+ "needsMoreThoughts": {"type": "boolean", "description": "If more thoughts are needed"}
+ },
+ "required": ["thought", "nextThoughtNeeded", "thoughtNumber", "totalThoughts"]
+ }
+ ```
+- **Implementation**:
+ - Track thought sequences and branching
+ - Support revision and refinement of previous thoughts
+ - Maintain context across thought progression
+ - Generate and verify solution hypotheses
+- **Output**: TextContent with thought processing results
+
+## 6. Time Server (`mcp-server-time`)
+
+### Dependencies
+- Timezone database (IANA timezones)
+- System timezone detection
+- Time parsing and formatting
+
+### Tools
+
+#### `get_current_time`
+- **Description**: Get current time in a specific timezone
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "timezone": {"type": "string", "description": "IANA timezone name (e.g., 'America/New_York', 'Europe/London')"}
+ },
+ "required": ["timezone"]
+ }
+ ```
+- **Implementation**: Get current time in specified timezone with DST information
+- **Output**: JSON TextContent with timezone, datetime (ISO format), is_dst
+
+#### `convert_time`
+- **Description**: Convert time between timezones
+- **Input Schema**:
+ ```json
+ {
+ "type": "object",
+ "properties": {
+ "source_timezone": {"type": "string", "description": "Source IANA timezone name"},
+ "time": {"type": "string", "description": "Time in 24-hour format (HH:MM)"},
+ "target_timezone": {"type": "string", "description": "Target IANA timezone name"}
+ },
+ "required": ["source_timezone", "time", "target_timezone"]
+ }
+ ```
+- **Implementation**: Convert time between timezones, calculate time difference
+- **Output**: JSON TextContent with source and target time information plus difference
+
+### Configuration
+- Command line argument: `--local-timezone` to override system timezone detection
+
+## Common Go Implementation Requirements
+
+### JSON-RPC 2.0 Implementation
+- Message framing over stdio
+- Request/response correlation with IDs
+- Notification handling (no response expected)
+- Error response formatting
+
+### Shared Types
+```go
+type TextContent struct {
+ Type string `json:"type"`
+ Text string `json:"text"`
+}
+
+type Tool struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ InputSchema interface{} `json:"inputSchema"`
+}
+
+type ServerInfo struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+}
+```
+
+### Error Handling
+- Standard JSON-RPC error codes
+- Descriptive error messages
+- Graceful degradation
+- Input validation with clear error messages
+
+### Logging and Debugging
+- Structured logging support
+- Debug mode for development
+- Request/response tracing capability
+
+### Configuration
+- Command line argument parsing
+- Environment variable support
+- Configuration file support where appropriate
+- Sensible defaults
+
+### Testing Requirements
+- Unit tests for all tools
+- Integration tests with MCP clients
+- Error condition testing
+- Mock external dependencies where needed
+
+This specification provides the foundation for implementing full-featured MCP servers in Go with complete compatibility with the Python reference implementations.
\ No newline at end of file
pkg/mcp/server.go
@@ -0,0 +1,340 @@
+package mcp
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "sync"
+)
+
+// Server represents an MCP server
+type Server struct {
+ name string
+ version string
+ capabilities ServerCapabilities
+
+ // Handler functions
+ toolHandlers map[string]ToolHandler
+ promptHandlers map[string]PromptHandler
+ resourceHandlers map[string]ResourceHandler
+
+ // Lifecycle handlers
+ initializeHandler func(InitializeRequest) (InitializeResult, error)
+ shutdownHandler func() error
+
+ mu sync.RWMutex
+}
+
+// Handler types
+type ToolHandler func(CallToolRequest) (CallToolResult, error)
+type PromptHandler func(GetPromptRequest) (GetPromptResult, error)
+type ResourceHandler func(ReadResourceRequest) (ReadResourceResult, error)
+
+// NewServer creates a new MCP server
+func NewServer(name, version string) *Server {
+ return &Server{
+ name: name,
+ version: version,
+ toolHandlers: make(map[string]ToolHandler),
+ promptHandlers: make(map[string]PromptHandler),
+ resourceHandlers: make(map[string]ResourceHandler),
+ capabilities: ServerCapabilities{
+ Tools: &ToolsCapability{},
+ Prompts: &PromptsCapability{},
+ Resources: &ResourcesCapability{},
+ Logging: &LoggingCapability{},
+ },
+ }
+}
+
+// RegisterTool registers a tool handler
+func (s *Server) RegisterTool(name string, handler ToolHandler) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.toolHandlers[name] = handler
+}
+
+// RegisterPrompt registers a prompt handler
+func (s *Server) RegisterPrompt(name string, handler PromptHandler) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.promptHandlers[name] = handler
+}
+
+// RegisterResource registers a resource handler
+func (s *Server) RegisterResource(uri string, handler ResourceHandler) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.resourceHandlers[uri] = handler
+}
+
+// SetInitializeHandler sets the initialize handler
+func (s *Server) SetInitializeHandler(handler func(InitializeRequest) (InitializeResult, error)) {
+ s.initializeHandler = handler
+}
+
+// SetShutdownHandler sets the shutdown handler
+func (s *Server) SetShutdownHandler(handler func() error) {
+ s.shutdownHandler = handler
+}
+
+// ListTools returns all registered tools
+func (s *Server) ListTools() []Tool {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ tools := make([]Tool, 0, len(s.toolHandlers))
+ for name := range s.toolHandlers {
+ // Tools will be defined by individual servers
+ // This is a placeholder - real implementation would store tool definitions
+ tools = append(tools, Tool{
+ Name: name,
+ Description: "Tool: " + name,
+ InputSchema: map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{},
+ },
+ })
+ }
+
+ return tools
+}
+
+// Run starts the server and handles JSON-RPC over stdio
+func (s *Server) Run(ctx context.Context) error {
+ scanner := bufio.NewScanner(os.Stdin)
+ encoder := json.NewEncoder(os.Stdout)
+
+ for scanner.Scan() {
+ line := scanner.Bytes()
+
+ var req JSONRPCRequest
+ if err := json.Unmarshal(line, &req); err != nil {
+ s.sendError(encoder, nil, ParseError, "Parse error")
+ continue
+ }
+
+ response := s.handleRequest(req)
+ if err := encoder.Encode(response); err != nil {
+ log.Printf("Error encoding response: %v", err)
+ }
+
+ // Check for shutdown
+ if req.Method == "shutdown" {
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return fmt.Errorf("scanner error: %w", err)
+ }
+
+ return nil
+}
+
+// handleRequest processes a JSON-RPC request
+func (s *Server) handleRequest(req JSONRPCRequest) JSONRPCResponse {
+ switch req.Method {
+ case "initialize":
+ return s.handleInitialize(req)
+ case "initialized":
+ return s.handleInitialized(req)
+ case "shutdown":
+ return s.handleShutdown(req)
+ case "tools/list":
+ return s.handleListTools(req)
+ case "tools/call":
+ return s.handleCallTool(req)
+ case "prompts/list":
+ return s.handleListPrompts(req)
+ case "prompts/get":
+ return s.handleGetPrompt(req)
+ case "resources/list":
+ return s.handleListResources(req)
+ case "resources/read":
+ return s.handleReadResource(req)
+ default:
+ return s.createErrorResponse(req.ID, MethodNotFound, "Method not found")
+ }
+}
+
+func (s *Server) handleInitialize(req JSONRPCRequest) JSONRPCResponse {
+ var initReq InitializeRequest
+ if err := json.Unmarshal(req.Params, &initReq); err != nil {
+ return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
+ }
+
+ result := InitializeResult{
+ ProtocolVersion: "2024-11-05",
+ Capabilities: s.capabilities,
+ ServerInfo: ServerInfo{
+ Name: s.name,
+ Version: s.version,
+ },
+ }
+
+ if s.initializeHandler != nil {
+ if customResult, err := s.initializeHandler(initReq); err == nil {
+ result = customResult
+ }
+ }
+
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) handleInitialized(req JSONRPCRequest) JSONRPCResponse {
+ // Initialized notification - no response needed
+ return JSONRPCResponse{
+ JSONRPC: "2.0",
+ ID: req.ID,
+ }
+}
+
+func (s *Server) handleShutdown(req JSONRPCRequest) JSONRPCResponse {
+ if s.shutdownHandler != nil {
+ s.shutdownHandler()
+ }
+
+ return s.createSuccessResponse(req.ID, nil)
+}
+
+func (s *Server) handleListTools(req JSONRPCRequest) JSONRPCResponse {
+ tools := s.ListTools()
+ result := ListToolsResult{Tools: tools}
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) handleCallTool(req JSONRPCRequest) JSONRPCResponse {
+ var callReq CallToolRequest
+ if err := json.Unmarshal(req.Params, &callReq); err != nil {
+ return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
+ }
+
+ s.mu.RLock()
+ handler, exists := s.toolHandlers[callReq.Name]
+ s.mu.RUnlock()
+
+ if !exists {
+ return s.createErrorResponse(req.ID, MethodNotFound, "Tool not found")
+ }
+
+ result, err := handler(callReq)
+ if err != nil {
+ return s.createErrorResponse(req.ID, InternalError, err.Error())
+ }
+
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) handleListPrompts(req JSONRPCRequest) JSONRPCResponse {
+ // Return empty prompts list for now
+ result := ListPromptsResult{Prompts: []Prompt{}}
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) handleGetPrompt(req JSONRPCRequest) JSONRPCResponse {
+ var promptReq GetPromptRequest
+ if err := json.Unmarshal(req.Params, &promptReq); err != nil {
+ return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
+ }
+
+ s.mu.RLock()
+ handler, exists := s.promptHandlers[promptReq.Name]
+ s.mu.RUnlock()
+
+ if !exists {
+ return s.createErrorResponse(req.ID, MethodNotFound, "Prompt not found")
+ }
+
+ result, err := handler(promptReq)
+ if err != nil {
+ return s.createErrorResponse(req.ID, InternalError, err.Error())
+ }
+
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) handleListResources(req JSONRPCRequest) JSONRPCResponse {
+ // Return empty resources list for now
+ result := ListResourcesResult{Resources: []Resource{}}
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) handleReadResource(req JSONRPCRequest) JSONRPCResponse {
+ var readReq ReadResourceRequest
+ if err := json.Unmarshal(req.Params, &readReq); err != nil {
+ return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
+ }
+
+ s.mu.RLock()
+ handler, exists := s.resourceHandlers[readReq.URI]
+ s.mu.RUnlock()
+
+ if !exists {
+ return s.createErrorResponse(req.ID, MethodNotFound, "Resource not found")
+ }
+
+ result, err := handler(readReq)
+ if err != nil {
+ return s.createErrorResponse(req.ID, InternalError, err.Error())
+ }
+
+ return s.createSuccessResponse(req.ID, result)
+}
+
+func (s *Server) createSuccessResponse(id *int, result interface{}) JSONRPCResponse {
+ var resultBytes *json.RawMessage
+ if result != nil {
+ bytes, _ := json.Marshal(result)
+ rawMsg := json.RawMessage(bytes)
+ resultBytes = &rawMsg
+ }
+
+ return JSONRPCResponse{
+ JSONRPC: "2.0",
+ ID: id,
+ Result: resultBytes,
+ }
+}
+
+func (s *Server) createErrorResponse(id *int, code int, message string) JSONRPCResponse {
+ return JSONRPCResponse{
+ JSONRPC: "2.0",
+ ID: id,
+ Error: &JSONRPCError{
+ Code: code,
+ Message: message,
+ },
+ }
+}
+
+func (s *Server) sendError(encoder *json.Encoder, id *int, code int, message string) {
+ response := s.createErrorResponse(id, code, message)
+ encoder.Encode(response)
+}
+
+// Helper function to create text content
+func NewTextContent(text string) TextContent {
+ return TextContent{
+ Type: "text",
+ Text: text,
+ }
+}
+
+// Helper function to create tool result
+func NewToolResult(content ...Content) CallToolResult {
+ return CallToolResult{
+ Content: content,
+ }
+}
+
+// Helper function to create tool error result
+func NewToolError(message string) CallToolResult {
+ return CallToolResult{
+ Content: []Content{NewTextContent("Error: " + message)},
+ IsError: true,
+ }
+}
\ No newline at end of file
pkg/mcp/types.go
@@ -0,0 +1,228 @@
+package mcp
+
+import (
+ "encoding/json"
+)
+
+// JSON-RPC 2.0 types
+type JSONRPCRequest struct {
+ JSONRPC string `json:"jsonrpc"`
+ ID *int `json:"id,omitempty"`
+ Method string `json:"method"`
+ Params json.RawMessage `json:"params,omitempty"`
+}
+
+type JSONRPCResponse struct {
+ JSONRPC string `json:"jsonrpc"`
+ ID *int `json:"id,omitempty"`
+ Result *json.RawMessage `json:"result,omitempty"`
+ Error *JSONRPCError `json:"error,omitempty"`
+}
+
+type JSONRPCError struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data interface{} `json:"data,omitempty"`
+}
+
+// JSON-RPC error codes
+const (
+ ParseError = -32700
+ InvalidRequest = -32600
+ MethodNotFound = -32601
+ InvalidParams = -32602
+ InternalError = -32603
+)
+
+// MCP Protocol types
+type InitializeRequest struct {
+ ProtocolVersion string `json:"protocolVersion"`
+ Capabilities Capabilities `json:"capabilities"`
+ ClientInfo ClientInfo `json:"clientInfo"`
+}
+
+type Capabilities struct {
+ Roots *RootsCapability `json:"roots,omitempty"`
+ Sampling *SamplingCapability `json:"sampling,omitempty"`
+ Experimental map[string]interface{} `json:"experimental,omitempty"`
+}
+
+type RootsCapability struct {
+ ListChanged bool `json:"listChanged,omitempty"`
+}
+
+type SamplingCapability struct{}
+
+type ClientInfo struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+}
+
+type InitializeResult struct {
+ ProtocolVersion string `json:"protocolVersion"`
+ Capabilities ServerCapabilities `json:"capabilities"`
+ ServerInfo ServerInfo `json:"serverInfo"`
+ Instructions string `json:"instructions,omitempty"`
+}
+
+type ServerCapabilities struct {
+ Logging *LoggingCapability `json:"logging,omitempty"`
+ Prompts *PromptsCapability `json:"prompts,omitempty"`
+ Resources *ResourcesCapability `json:"resources,omitempty"`
+ Tools *ToolsCapability `json:"tools,omitempty"`
+}
+
+type LoggingCapability struct{}
+type PromptsCapability struct {
+ ListChanged bool `json:"listChanged,omitempty"`
+}
+type ResourcesCapability struct {
+ Subscribe bool `json:"subscribe,omitempty"`
+ ListChanged bool `json:"listChanged,omitempty"`
+}
+type ToolsCapability struct {
+ ListChanged bool `json:"listChanged,omitempty"`
+}
+
+type ServerInfo struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+}
+
+// Tool types
+type Tool struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ InputSchema interface{} `json:"inputSchema"`
+}
+
+type ListToolsResult struct {
+ Tools []Tool `json:"tools"`
+}
+
+type CallToolRequest struct {
+ Name string `json:"name"`
+ Arguments map[string]interface{} `json:"arguments,omitempty"`
+}
+
+type CallToolResult struct {
+ Content []Content `json:"content"`
+ IsError bool `json:"isError,omitempty"`
+}
+
+// Content types
+type Content interface {
+ GetType() string
+}
+
+type TextContent struct {
+ Type string `json:"type"`
+ Text string `json:"text"`
+}
+
+func (t TextContent) GetType() string {
+ return t.Type
+}
+
+type ImageContent struct {
+ Type string `json:"type"`
+ Data string `json:"data"`
+ MimeType string `json:"mimeType"`
+}
+
+func (i ImageContent) GetType() string {
+ return i.Type
+}
+
+// Prompt types
+type Prompt struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Arguments []PromptArgument `json:"arguments,omitempty"`
+}
+
+type PromptArgument struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Required bool `json:"required,omitempty"`
+}
+
+type ListPromptsResult struct {
+ Prompts []Prompt `json:"prompts"`
+}
+
+type GetPromptRequest struct {
+ Name string `json:"name"`
+ Arguments map[string]interface{} `json:"arguments,omitempty"`
+}
+
+type PromptMessage struct {
+ Role string `json:"role"`
+ Content Content `json:"content"`
+}
+
+type GetPromptResult struct {
+ Description string `json:"description,omitempty"`
+ Messages []PromptMessage `json:"messages"`
+}
+
+// Resource types
+type Resource struct {
+ URI string `json:"uri"`
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ MimeType string `json:"mimeType,omitempty"`
+}
+
+type ListResourcesResult struct {
+ Resources []Resource `json:"resources"`
+}
+
+type ReadResourceRequest struct {
+ URI string `json:"uri"`
+}
+
+type ReadResourceResult struct {
+ Contents []Content `json:"contents"`
+}
+
+// Root types
+type Root struct {
+ URI string `json:"uri"`
+ Name string `json:"name"`
+}
+
+type ListRootsResult struct {
+ Roots []Root `json:"roots"`
+}
+
+// Logging types
+type LoggingLevel string
+
+const (
+ LoggingLevelDebug LoggingLevel = "debug"
+ LoggingLevelInfo LoggingLevel = "info"
+ LoggingLevelNotice LoggingLevel = "notice"
+ LoggingLevelWarning LoggingLevel = "warning"
+ LoggingLevelError LoggingLevel = "error"
+ LoggingLevelCrit LoggingLevel = "crit"
+ LoggingLevelAlert LoggingLevel = "alert"
+ LoggingLevelEmerg LoggingLevel = "emerg"
+)
+
+type SetLoggingLevelRequest struct {
+ Level LoggingLevel `json:"level"`
+}
+
+// Notification types
+type LoggingMessageNotification struct {
+ Level LoggingLevel `json:"level"`
+ Data interface{} `json:"data"`
+ Logger string `json:"logger,omitempty"`
+}
+
+type ProgressNotification struct {
+ ProgressToken string `json:"progressToken"`
+ Progress float64 `json:"progress"`
+ Total float64 `json:"total,omitempty"`
+}
\ No newline at end of file
.gitignore
@@ -0,0 +1,35 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+# Build output
+bin/
+dist/
+
+# Memory file for development
+memory.json
+
+# IDE files
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS files
+.DS_Store
+Thumbs.db
\ No newline at end of file
go.mod
@@ -0,0 +1,3 @@
+module github.com/xlgmokha/mcp
+
+go 1.24.0