Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions cmd/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"fmt"
"github.com/mcpjungle/mcpjungle/pkg/types"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"io"
"os"
"path/filepath"
)

var (
Expand Down Expand Up @@ -88,13 +91,48 @@ func init() {
func readMcpServerConfig(filePath string) (types.RegisterServerInput, error) {
var input types.RegisterServerInput

data, err := os.ReadFile(filePath)
f, err := os.Open(filePath)
if err != nil {
return input, fmt.Errorf("failed to read config file %s: %w", registerCmdServerConfigFilePath, err)
return input, fmt.Errorf("failed to open config file: %w", err)
}
defer f.Close()
ext := filepath.Ext(filePath)
switch ext {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, do you think it is better to take out the whole configuration parsing logic into a separate package?
There are also some other commands that now rely on finding & parsing configurations.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this logic should be reuseble.

case ".yaml", ".yml":
return readMcpServerConfigYaml(f)
case ".json":
return readMcpServerConfigJson(f)
default:
fmt.Println("Unknown server config file extension. Assuming json format.")
return readMcpServerConfigJson(f)
}
}

func readMcpServerConfigJson(reader io.Reader) (types.RegisterServerInput, error) {
var input types.RegisterServerInput

data, err := io.ReadAll(reader)
if err != nil {
return input, fmt.Errorf("failed to read config from config file: %w", err)
}
// Parse JSON config
if err := json.Unmarshal(data, &input); err != nil {
return input, fmt.Errorf("failed to parse config file: %w", err)
return input, fmt.Errorf("failed to parse config from config file: %w", err)
}

return input, nil
}

func readMcpServerConfigYaml(reader io.Reader) (types.RegisterServerInput, error) {
var input types.RegisterServerInput

data, err := io.ReadAll(reader)
if err != nil {
return input, fmt.Errorf("failed to read config from config file: %w", err)
}
// Parse YAML config
if err := yaml.Unmarshal(data, &input); err != nil {
return input, fmt.Errorf("failed to parse config from config file: %w", err)
}

return input, nil
Expand Down
98 changes: 98 additions & 0 deletions cmd/register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cmd

import (
"strings"
"testing"

"github.com/mcpjungle/mcpjungle/pkg/types"
)

func TestReadMcpServerConfigJson(t *testing.T) {
input := `{
"name": "test_server",
"transport": "stdio",
"description": "Test JSON server",
"command": "node",
"args": ["server.js"],
"env": {"NODE_ENV": "test"}
}`

reader := strings.NewReader(input)
got, err := readMcpServerConfigJson(reader)
if err != nil {
t.Fatalf("readMcpServerConfigJson() error = %v", err)
}

expected := types.RegisterServerInput{
Name: "test_server",
Transport: "stdio",
Description: "Test JSON server",
Command: "node",
Args: []string{"server.js"},
Env: map[string]string{"NODE_ENV": "test"},
}

if got.Name != expected.Name {
t.Errorf("Name = %q, want %q", got.Name, expected.Name)
}
if got.Transport != expected.Transport {
t.Errorf("Transport = %q, want %q", got.Transport, expected.Transport)
}
if got.Description != expected.Description {
t.Errorf("Description = %q, want %q", got.Description, expected.Description)
}
if got.Command != expected.Command {
t.Errorf("Command = %q, want %q", got.Command, expected.Command)
}
if len(got.Args) != len(expected.Args) || got.Args[0] != expected.Args[0] {
t.Errorf("Args = %v, want %v", got.Args, expected.Args)
}
if got.Env["NODE_ENV"] != expected.Env["NODE_ENV"] {
t.Errorf("Env = %v, want %v", got.Env, expected.Env)
}
}

func TestReadMcpServerConfigYaml(t *testing.T) {
input := `name: test_server
transport: stdio
description: Test YAML server
command: node
args:
- server.js
env:
NODE_ENV: test`

reader := strings.NewReader(input)
got, err := readMcpServerConfigYaml(reader)
if err != nil {
t.Fatalf("readMcpServerConfigYaml() error = %v", err)
}

expected := types.RegisterServerInput{
Name: "test_server",
Transport: "stdio",
Description: "Test YAML server",
Command: "node",
Args: []string{"server.js"},
Env: map[string]string{"NODE_ENV": "test"},
}

if got.Name != expected.Name {
t.Errorf("Name = %q, want %q", got.Name, expected.Name)
}
if got.Transport != expected.Transport {
t.Errorf("Transport = %q, want %q", got.Transport, expected.Transport)
}
if got.Description != expected.Description {
t.Errorf("Description = %q, want %q", got.Description, expected.Description)
}
if got.Command != expected.Command {
t.Errorf("Command = %q, want %q", got.Command, expected.Command)
}
if len(got.Args) != len(expected.Args) || got.Args[0] != expected.Args[0] {
t.Errorf("Args = %v, want %v", got.Args, expected.Args)
}
if got.Env["NODE_ENV"] != expected.Env["NODE_ENV"] {
t.Errorf("Env = %v, want %v", got.Env, expected.Env)
}
}
16 changes: 8 additions & 8 deletions pkg/types/mcp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,34 @@ type McpServer struct {
// It is also the basis for the JSON configuration file used to register a new MCP server.
type RegisterServerInput struct {
// Name is the unique name of an MCP server registered in mcpjungle
Name string `json:"name"`
Name string `json:"name" yaml:"name"`

// Transport is the transport protocol used by the MCP server.
// valid values are "stdio", "streamable_http""
Transport string `json:"transport"`
Transport string `json:"transport" yaml:"transport"`

Description string `json:"description"`
Description string `json:"description" yaml:"description"`

// URL is the URL of the remote mcp server
// It is mandatory when transport is streamable_http and must be a valid
// http/https URL (e.g., https://example.com/mcp).
URL string `json:"url"`
URL string `json:"url" yaml:"url"`

// BearerToken is an optional token used for authenticating requests to the remote MCP server.
// It is useful when the upstream MCP server requires static tokens (e.g., API tokens) for authentication.
// If the transport is "stdio", this field is ignored.
BearerToken string `json:"bearer_token"`
BearerToken string `json:"bearer_token" yaml:"bearer_token"`

// Command is the command to run the mcp server.
// It is mandatory when the transport is "stdio".
Command string `json:"command"`
Command string `json:"command" yaml:"command"`

// Args is the list of arguments to pass to the command when the transport is "stdio".
Args []string `json:"args"`
Args []string `json:"args" yaml:"args"`

// Env is the set of environment variables to pass to the mcp server when the transport is "stdio".
// Both the key and value must be of type string.
Env map[string]string `json:"env"`
Env map[string]string `json:"env" yaml:"env"`
}

// ValidateTransport validates the input string and returns the corresponding model.McpServerTransport.
Expand Down