vibe-proxy/backend/internal/runtime/executor/helps/claude_mcp_alias.go
2026-08-24 00:10:41 +02:00

144 lines
4.9 KiB
Go

package helps
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"strings"
log "github.com/sirupsen/logrus"
)
// IsClaudeMCPToolName reports whether name follows Claude Code's MCP tool
// convention and contains only characters accepted by Anthropic tool names.
func IsClaudeMCPToolName(name string) bool {
if len(name) == 0 || len(name) > 64 || !strings.HasPrefix(name, "mcp__") {
return false
}
rest := strings.TrimPrefix(name, "mcp__")
separator := strings.Index(rest, "__")
if separator <= 0 || separator+2 >= len(rest) {
return false
}
for _, char := range name {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '_' || char == '-' {
continue
}
return false
}
return true
}
// ClaudeMCPAliasWordCount is the BIP-39 English dictionary size used for the
// virtual server pair and the one-word tool ID.
func ClaudeMCPAliasWordCount() int {
return len(claudeMCPAliasEnglishWords)
}
// ClaudeMCPToolAlias derives a Claude Code-style MCP tool name. Aliases from
// one caller share a virtual server component. The tool component combines a
// stable keyed ID with a truncated semantic suffix so the model can distinguish
// tools by name while the request-local symbol table restores the exact original.
// A higher attempt linearly probes the next word when a collision must be avoided.
// Server and tool IDs use BIP-39 English words so weak models are less likely
// to drift high-entropy Base32 fragments.
func ClaudeMCPToolAlias(secret, original string, attempt uint32) string {
toolDigest := claudeMCPAliasDigest(secret, "tool", original)
return claudeMCPAliasFor(
claudeMCPAliasServerComponent(secret),
claudeMCPAliasWord(toolDigest[:], 0, attempt),
original,
)
}
// AllocateClaudeMCPToolAlias picks an alias that is not already reserved.
// Attempts are capped at the wordlist size so names that sanitize to the same
// suffix cannot spin forever. ok is false only when every one-word tool ID for
// this semantic is already reserved.
func AllocateClaudeMCPToolAlias(secret, original string, reserved map[string]bool) (string, bool) {
words := claudeMCPAliasEnglishWords
totalWords := len(words)
if totalWords == 0 {
log.Error("claude oauth mcp alias: embedded BIP-39 wordlist is empty, tool aliasing is disabled")
return "", false
}
server := claudeMCPAliasServerComponent(secret)
toolDigest := claudeMCPAliasDigest(secret, "tool", original)
baseIndex := int(binary.BigEndian.Uint16(toolDigest[0:2])) % totalWords
for attempt := 0; attempt < totalWords; attempt++ {
alias := claudeMCPAliasFor(server, words[(baseIndex+attempt)%totalWords], original)
if reserved != nil && reserved[alias] {
continue
}
return alias, true
}
return "", false
}
// claudeMCPAliasFor assembles the final alias for one server/tool word pair.
// Both the single-shot and the allocating entry point must build names here so
// the two cannot drift apart.
func claudeMCPAliasFor(server, toolID, original string) string {
prefix := "mcp__" + server + "__" + toolID + "_"
maxSemanticLen := 64 - len(prefix)
if maxSemanticLen < 1 {
maxSemanticLen = 1
}
return prefix + claudeMCPToolSemanticSuffix(original, maxSemanticLen)
}
// claudeMCPAliasServerComponent derives the caller-stable two-word virtual
// server shared by every alias generated for one credential.
func claudeMCPAliasServerComponent(secret string) string {
serverDigest := claudeMCPAliasDigest(secret, "server", "")
return claudeMCPAliasWord(serverDigest[:], 0, 0) + "_" + claudeMCPAliasWord(serverDigest[:], 2, 0)
}
func claudeMCPAliasWord(digest []byte, offset int, attempt uint32) string {
words := claudeMCPAliasEnglishWords
if len(words) == 0 || offset < 0 || offset+2 > len(digest) {
return "tool"
}
base := int(binary.BigEndian.Uint16(digest[offset : offset+2]))
return words[(base+int(attempt))%len(words)]
}
func claudeMCPToolSemanticSuffix(original string, maxLength int) string {
var semantic strings.Builder
semantic.Grow(min(len(original), maxLength))
pendingSeparator := false
for _, char := range original {
valid := (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '_' || char == '-'
if !valid {
pendingSeparator = semantic.Len() > 0
continue
}
if pendingSeparator && semantic.Len()+1 < maxLength {
semantic.WriteByte('_')
}
pendingSeparator = false
if semantic.Len() >= maxLength {
break
}
semantic.WriteRune(char)
}
result := strings.Trim(semantic.String(), "_-")
if result == "" {
return "tool"
}
return result
}
func claudeMCPAliasDigest(secret, purpose, original string) [sha256.Size]byte {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte("cpa-claude-mcp-alias-v2\x00"))
_, _ = mac.Write([]byte(purpose))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(original))
var digest [sha256.Size]byte
copy(digest[:], mac.Sum(nil))
return digest
}