diff --git a/publisher b/publisher index fd639bc..f8fa76b 100755 Binary files a/publisher and b/publisher differ diff --git a/tools/publisher/README.md b/tools/publisher/README.md index df14492..114611c 100644 --- a/tools/publisher/README.md +++ b/tools/publisher/README.md @@ -18,12 +18,23 @@ The compiled binary will be placed in the `bin` directory. ## Usage +The tool supports two main commands: + +### Publishing a server + ```bash # Basic usage -./bin/mcp-publisher -registry-url -mcp-file +./bin/mcp-publisher publish -registry-url -mcp-file # Force a new login even if a token exists -./bin/mcp-publisher -registry-url -mcp-file -login +./bin/mcp-publisher publish -registry-url -mcp-file -login +``` + +### Creating a server.json file + +```bash +# Create a new server.json file +./bin/mcp-publisher create --name "io.github.owner/repo" --description "My server" --repo-url "https://github.com/owner/repo" ``` ### Command-line Arguments @@ -33,6 +44,87 @@ The compiled binary will be placed in the `bin` directory. - `-login`: Force a new GitHub authentication even if a token already exists (overwrites existing token file) - `-auth-method`: Authentication method to use (default: github-oauth) +## Creating a server.json file + +The tool provides a `create` command to help generate a properly formatted `server.json` file. This command takes various flags to specify the server details and generates a complete server.json file that you can then modify as needed. + +### Usage + +```bash +./bin/mcp-publisher create [flags] +``` + +### Create Command Flags + +#### Required Flags +- `--name`, `-n`: Server name (e.g., io.github.owner/repo-name) +- `--description`, `-d`: Server description +- `--repo-url`: Repository URL + +#### Optional Flags +- `--version`, `-v`: Server version (default: "1.0.0") +- `--repo-source`: Repository source (default: "github") +- `--output`, `-o`: Output file path (default: "server.json") +- `--execute`, `-e`: Command to execute the server (generates runtime arguments) +- `--registry`: Package registry name (default: "npm") +- `--package-name`: Package name (defaults to server name) +- `--package-version`: Package version (defaults to server version) +- `--runtime-hint`: Runtime hint (e.g., "docker") +- `--env-var`: Environment variable in format NAME:DESCRIPTION (can be repeated) +- `--package-arg`: Package argument in format VALUE:DESCRIPTION (can be repeated) + +### Create Examples + +#### Basic NPX Server + +```bash +./bin/mcp-publisher create \ + --name "io.github.example/my-server" \ + --description "My MCP server" \ + --repo-url "https://github.com/example/my-server" \ + --execute "npx @example/my-server --verbose" \ + --env-var "API_KEY:Your API key for the service" +``` + +#### Docker Server + +```bash +./bin/mcp-publisher create \ + --name "io.github.example/docker-server" \ + --description "Docker-based MCP server" \ + --repo-url "https://github.com/example/docker-server" \ + --runtime-hint "docker" \ + --execute "docker run --mount type=bind,src=/data,dst=/app/data example/server" \ + --env-var "CONFIG_PATH:Path to configuration file" +``` + +#### Server with Package Arguments + +```bash +./bin/mcp-publisher create \ + --name "io.github.example/full-server" \ + --description "Complete server example" \ + --repo-url "https://github.com/example/full-server" \ + --execute "npx @example/server" \ + --package-arg "-s:Specify services and permissions" \ + --package-arg "--config:Configuration file path" \ + --env-var "API_KEY:Service API key" \ + --env-var "DEBUG:Enable debug mode" +``` + +The `create` command will generate a `server.json` file with: +- Proper structure and formatting +- Runtime arguments parsed from the `--execute` command +- Environment variables with descriptions +- Package arguments for user configuration +- All necessary metadata + +After creation, you may need to manually edit the file to: +- Adjust argument descriptions and requirements +- Set environment variable optionality (`is_required`, `is_secret`) +- Add remote server configurations +- Fine-tune runtime and package arguments + ## Authentication The tool has been simplified to use **GitHub OAuth device flow authentication exclusively**. Previous versions supported multiple authentication methods, but this version focuses solely on GitHub OAuth for better security and user experience. @@ -45,7 +137,9 @@ The tool has been simplified to use **GitHub OAuth device flow authentication ex **Note**: Authentication is performed via GitHub OAuth App, which you must authorize for the respective resources (e.g., organization access if publishing organization repositories). -## Example +## Publishing Example + +To publish an existing server.json file to the registry: 1. Prepare your `server.json` file with your server details: @@ -90,7 +184,7 @@ The tool has been simplified to use **GitHub OAuth device flow authentication ex 2. Run the publisher tool: ```bash -./bin/mcp-publisher --registry-url "https://mcp-registry.example.com" --mcp-file "./server.json" +./bin/mcp-publisher publish --registry-url "https://mcp-registry.example.com" --mcp-file "./server.json" ``` 3. Follow the authentication instructions in the terminal if prompted. diff --git a/tools/publisher/main.go b/tools/publisher/main.go index 61ab08c..bb64058 100644 --- a/tools/publisher/main.go +++ b/tools/publisher/main.go @@ -47,6 +47,7 @@ type Package struct { Version string `json:"version"` RuntimeHint string `json:"runtime_hint,omitempty"` RuntimeArguments []RuntimeArgument `json:"runtime_arguments,omitempty"` + PackageArguments []RuntimeArgument `json:"package_arguments,omitempty"` EnvironmentVariables []EnvironmentVariable `json:"environment_variables,omitempty"` } @@ -170,6 +171,7 @@ func createCommand() { // Repeatable flags var envVars []string + var packageArgs []string createFlags.StringVar(&name, "name", "", "Server name (e.g., io.github.owner/repo-name) (required)") createFlags.StringVar(&name, "n", "", "Server name (shorthand)") @@ -194,6 +196,12 @@ func createCommand() { envVars = append(envVars, value) return nil }) + + // Custom flag for package arguments + createFlags.Func("package-arg", "Package argument in format VALUE:DESCRIPTION (can be repeated)", func(value string) error { + packageArgs = append(packageArgs, value) + return nil + }) createFlags.Parse(os.Args[2:]) @@ -217,7 +225,7 @@ func createCommand() { } // Create server structure - server := createServerStructure(name, description, version, repoURL, repoSource, registryName, packageName, packageVersion, runtimeHint, execute, envVars) + server := createServerStructure(name, description, version, repoURL, repoSource, registryName, packageName, packageVersion, runtimeHint, execute, envVars, packageArgs) // Convert to JSON jsonData, err := json.MarshalIndent(server, "", " ") @@ -292,7 +300,7 @@ func publishToRegistry(registryURL string, mcpData []byte, token string) error { return nil } -func createServerStructure(name, description, version, repoURL, repoSource, registryName, packageName, packageVersion, runtimeHint, execute string, envVars []string) ServerJSON { +func createServerStructure(name, description, version, repoURL, repoSource, registryName, packageName, packageVersion, runtimeHint, execute string, envVars []string, packageArgs []string) ServerJSON { // Parse environment variables var environmentVariables []EnvironmentVariable for _, envVar := range envVars { @@ -311,16 +319,50 @@ func createServerStructure(name, description, version, repoURL, repoSource, regi } } + // Parse package arguments + var packageArguments []RuntimeArgument + for i, pkgArg := range packageArgs { + parts := strings.SplitN(pkgArg, ":", 2) + value := parts[0] + description := fmt.Sprintf("Package argument %d", i+1) + if len(parts) == 2 { + description = parts[1] + } + + packageArguments = append(packageArguments, RuntimeArgument{ + Description: description, + IsRequired: true, // Package arguments are typically required + Format: "string", + Value: value, + Default: value, + Type: "positional", + ValueHint: value, + }) + } + // Parse execute command to create runtime arguments var runtimeArguments []RuntimeArgument if execute != "" { - // Split the execute command into parts - parts := strings.Fields(execute) + // Split the execute command into parts, handling quoted strings + parts := smartSplit(execute) if len(parts) > 1 { - // Add each argument as a runtime argument + // Skip the first part (command) and add each argument as a runtime argument for i, arg := range parts[1:] { + description := fmt.Sprintf("Runtime argument %d", i+1) + + // Try to provide better descriptions based on common patterns + if strings.HasPrefix(arg, "--") { + description = fmt.Sprintf("Command line flag: %s", arg) + } else if strings.HasPrefix(arg, "-") && len(arg) == 2 { + description = fmt.Sprintf("Command line option: %s", arg) + } else if strings.Contains(arg, "=") { + description = fmt.Sprintf("Configuration parameter: %s", arg) + } else if i > 0 && strings.HasPrefix(parts[i], "-") { + description = fmt.Sprintf("Value for %s", parts[i]) + } + runtimeArguments = append(runtimeArguments, RuntimeArgument{ - Description: fmt.Sprintf("Runtime argument %d", i+1), + Description: description, IsRequired: false, Format: "string", Value: arg, @@ -339,6 +381,7 @@ func createServerStructure(name, description, version, repoURL, repoSource, regi Version: packageVersion, RuntimeHint: runtimeHint, RuntimeArguments: runtimeArguments, + PackageArguments: packageArguments, EnvironmentVariables: environmentVariables, } @@ -356,3 +399,39 @@ func createServerStructure(name, description, version, repoURL, repoSource, regi Packages: []Package{pkg}, } } + +// smartSplit splits a command string into parts, handling quoted strings and common shell patterns +func smartSplit(command string) []string { + var parts []string + var current strings.Builder + var inQuotes bool + var quoteChar rune + + for _, char := range command { + switch { + case char == '"' || char == '\'': + if !inQuotes { + inQuotes = true + quoteChar = char + } else if char == quoteChar { + inQuotes = false + quoteChar = 0 + } else { + current.WriteRune(char) + } + case char == ' ' && !inQuotes: + if current.Len() > 0 { + parts = append(parts, current.String()) + current.Reset() + } + default: + current.WriteRune(char) + } + } + + if current.Len() > 0 { + parts = append(parts, current.String()) + } + + return parts +}