feat(schema): allow template variables in transport URL field (#910)
Authored by Claude Code Opus 4.5, steered/reviewed by me.
## Summary
- Updated the URL regex pattern in `StreamableHttpTransport` and
`SseTransport` to allow template variables like `{baseUrl}` as an
alternative to requiring the `https://` prefix
- This enables more flexible URL configuration in the `remotes` section
of server.json
## Changes
- Modified the `url` field pattern from `^https?://[^\s]+$` to
`^(https?://[^\s]+|\{[a-zA-Z_][a-zA-Z0-9_]*\}[^\s]*)$`
- Updated both `openapi.yaml` and the derived `server.schema.json`
- Added clarifying documentation that URL must start with `http://`,
`https://`, or a template variable
## Regex breakdown
```
^(https?://[^\s]+|\{[a-zA-Z_][a-zA-Z0-9_]*\}[^\s]*)$
```
| Part | Meaning |
|------|---------|
| `^` | Start of string |
| `(` | Begin alternation group |
| `https?://[^\s]+` | `http://` or `https://` followed by non-whitespace
|
| `\|` | OR |
| `\{[a-zA-Z_][a-zA-Z0-9_]*\}` | Template variable: `{` + valid
identifier + `}` |
| `[^\s]*` | Optional path/query after template variable |
| `)` | End alternation group |
| `$` | End of string |
## Test cases
**Should match ✓**
- `https://api.example.com/mcp`
- `http://localhost:8080/sse`
- `https://example.com/path?query=value`
- `{baseUrl}`
- `{baseUrl}/mcp`
- `{server_url}/api/v1`
- `{API_ENDPOINT}`
**Should NOT match ✗**
- `ftp://example.com` — wrong protocol
- `example.com` — missing protocol or variable
- `/relative/path` — relative paths not allowed
- `{invalid-name}/path` — hyphens not valid in variable names
- `{123invalid}` — variables can't start with numbers
---------
Co-authored-by: Agent Orchestrator <agent@ao.pulsemcp.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
This commit is contained in:
parent
541a6c61fa
commit
70a27717fb
1 changed files with 137 additions and 9 deletions
108
internal/validators/schema_regex_test.go
Normal file
108
internal/validators/schema_regex_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package validators_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const serverSchemaPath = "../../docs/reference/server-json/server.schema.json"
|
||||
|
||||
// schemaHelper provides utilities for extracting values from the JSON schema
|
||||
type schemaHelper struct {
|
||||
t *testing.T
|
||||
schema map[string]interface{}
|
||||
}
|
||||
|
||||
func loadSchema(t *testing.T) *schemaHelper {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(serverSchemaPath)
|
||||
require.NoError(t, err, "Failed to read schema file")
|
||||
|
||||
var schema map[string]interface{}
|
||||
err = json.Unmarshal(data, &schema)
|
||||
require.NoError(t, err, "Failed to parse schema JSON")
|
||||
|
||||
return &schemaHelper{t: t, schema: schema}
|
||||
}
|
||||
|
||||
// getDefinition returns a definition from the schema by name
|
||||
func (s *schemaHelper) getDefinition(name string) map[string]interface{} {
|
||||
s.t.Helper()
|
||||
definitions := s.schema["definitions"].(map[string]interface{})
|
||||
def, ok := definitions[name].(map[string]interface{})
|
||||
require.True(s.t, ok, "Definition %q not found in schema", name)
|
||||
return def
|
||||
}
|
||||
|
||||
// getPropertyPattern extracts a regex pattern from a definition's property
|
||||
func (s *schemaHelper) getPropertyPattern(definitionName, propertyName string) string {
|
||||
s.t.Helper()
|
||||
def := s.getDefinition(definitionName)
|
||||
props := def["properties"].(map[string]interface{})
|
||||
prop, ok := props[propertyName].(map[string]interface{})
|
||||
require.True(s.t, ok, "Property %q not found in %s", propertyName, definitionName)
|
||||
pattern, ok := prop["pattern"].(string)
|
||||
require.True(s.t, ok, "Pattern not found for %s.%s", definitionName, propertyName)
|
||||
return pattern
|
||||
}
|
||||
|
||||
// TestTransportURLPattern validates the URL pattern used by StreamableHttpTransport and SseTransport.
|
||||
// URLs must start with http://, https://, or a template variable like {baseUrl}.
|
||||
func TestTransportURLPattern(t *testing.T) {
|
||||
schema := loadSchema(t)
|
||||
|
||||
streamablePattern := schema.getPropertyPattern("StreamableHttpTransport", "url")
|
||||
ssePattern := schema.getPropertyPattern("SseTransport", "url")
|
||||
|
||||
// Verify both transport types use the same pattern
|
||||
assert.Equal(t, streamablePattern, ssePattern,
|
||||
"StreamableHttpTransport and SseTransport should use identical URL patterns")
|
||||
|
||||
t.Logf("Pattern: %s", streamablePattern)
|
||||
|
||||
re, err := regexp.Compile(streamablePattern)
|
||||
require.NoError(t, err, "Pattern should be valid regex")
|
||||
|
||||
// Test cases that SHOULD match
|
||||
validCases := []string{
|
||||
// Standard URLs
|
||||
"https://api.example.com/mcp",
|
||||
"http://localhost:8080/sse",
|
||||
"https://example.com/path?query=value",
|
||||
"https://api.example.com/v1/mcp",
|
||||
// Template variables
|
||||
"{baseUrl}",
|
||||
"{baseUrl}/mcp",
|
||||
"{server_url}/api/v1",
|
||||
"{API_ENDPOINT}",
|
||||
"{a}",
|
||||
"{_private}/endpoint",
|
||||
}
|
||||
|
||||
for _, tc := range validCases {
|
||||
assert.True(t, re.MatchString(tc), "Expected %q to match pattern", tc)
|
||||
}
|
||||
|
||||
// Test cases that should NOT match
|
||||
invalidCases := []string{
|
||||
"ftp://example.com", // wrong protocol
|
||||
"example.com", // missing protocol or variable
|
||||
"/relative/path", // relative path
|
||||
"{invalid-name}/path", // hyphen in variable name
|
||||
"{123invalid}", // variable starts with number
|
||||
"", // empty string
|
||||
"mailto:test@example.com", // wrong protocol
|
||||
"file:///path/to/file", // wrong protocol
|
||||
"{}/empty", // empty variable name
|
||||
"{{nested}}/path", // nested braces
|
||||
}
|
||||
|
||||
for _, tc := range invalidCases {
|
||||
assert.False(t, re.MatchString(tc), "Expected %q to NOT match pattern", tc)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue