将 test/pre-mcp-discovery 分支上多位同学的能力改动整体合入 main, 服务发现端点保持生产 https://mcp.dingtalk.com,未携带分支上的预发改动。 主要内容: - 修雨域(chat / report / todo / contact / mail)对齐 wukong 基线 (#355) - DWS 内容生产能力增强 (#387)、文档导出与 OA 对齐 (#362 #388 #390) - envelope 注册 cli.Aliases 为 cobra aliases、命令结构与 JSON 解析优化 - skills 多包同步 wukong 对齐内容(attendance/calendar/minutes/oa/sheet 等) - product/oa 进一步对齐 wukong 排除项:未携带 566b4e1(test: point service discovery to pre-mcp.dingtalk.com); 以下 4 个常量保持生产:skill_command.go / auth/endpoints.go / cli/loader.go / market/registry.go。 Co-authored-by: john <32427341+wqyenjoy@users.noreply.github.com> Co-authored-by: wxianfeng <wang.fl1429@gmail.com>
579 lines
19 KiB
Go
579 lines
19 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package ir
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/discovery"
|
|
)
|
|
|
|
type Catalog struct {
|
|
Products []CanonicalProduct `json:"products"`
|
|
}
|
|
|
|
type LifecycleInfo struct {
|
|
DeprecatedBy int `json:"deprecated_by,omitempty"`
|
|
DeprecationDate string `json:"deprecation_date,omitempty"`
|
|
MigrationURL string `json:"migration_url,omitempty"`
|
|
DeprecatedCandidate bool `json:"deprecated_candidate,omitempty"`
|
|
}
|
|
|
|
type ProductCLIMetadata struct {
|
|
Command string `json:"command,omitempty"`
|
|
Group string `json:"group,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
Skip bool `json:"skip,omitempty"`
|
|
Aliases []string `json:"aliases,omitempty"`
|
|
Prefixes []string `json:"prefixes,omitempty"`
|
|
}
|
|
|
|
type CLIFlagHint struct {
|
|
Shorthand string `json:"shorthand,omitempty"`
|
|
Alias string `json:"alias,omitempty"`
|
|
}
|
|
|
|
// FlagOverlay carries CLI-layer transformation metadata for a single
|
|
// MCP parameter: the flag alias the user types, the transform applied
|
|
// before dispatch, env-var fallback, default value, and whether the flag
|
|
// is hidden from help. Sourced from market.CLIToolOverride.Flags.
|
|
type FlagOverlay struct {
|
|
Alias string `json:"alias,omitempty"`
|
|
Transform string `json:"transform,omitempty"`
|
|
TransformArgs map[string]any `json:"transform_args,omitempty"`
|
|
EnvDefault string `json:"env_default,omitempty"`
|
|
Default string `json:"default,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
}
|
|
|
|
// ToolAnnotations mirrors MCP 2025+ tool annotations. All hints are
|
|
// nullable: absence means "unknown", not "false". Populate only when the
|
|
// source has a clear signal — don't guess.
|
|
type ToolAnnotations struct {
|
|
DestructiveHint *bool `json:"destructive_hint,omitempty"`
|
|
ReadOnlyHint *bool `json:"read_only_hint,omitempty"`
|
|
IdempotentHint *bool `json:"idempotent_hint,omitempty"`
|
|
OpenWorldHint *bool `json:"open_world_hint,omitempty"`
|
|
}
|
|
|
|
type CanonicalProduct struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"display_name"`
|
|
Description string `json:"description,omitempty"`
|
|
ServerKey string `json:"server_key"`
|
|
Endpoint string `json:"endpoint"`
|
|
SchemaURI string `json:"schema_uri,omitempty"`
|
|
NegotiatedProtocolVersion string `json:"negotiated_protocol_version,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Degraded bool `json:"degraded"`
|
|
Lifecycle *LifecycleInfo `json:"lifecycle,omitempty"`
|
|
CLI *ProductCLIMetadata `json:"cli,omitempty"`
|
|
Tools []ToolDescriptor `json:"tools"`
|
|
}
|
|
|
|
type ToolDescriptor struct {
|
|
RPCName string `json:"rpc_name"`
|
|
CLIName string `json:"cli_name,omitempty"`
|
|
Group string `json:"group,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
InputSchema map[string]any `json:"input_schema,omitempty"`
|
|
OutputSchema map[string]any `json:"output_schema,omitempty"`
|
|
Sensitive bool `json:"sensitive"`
|
|
Auth *ToolAuthMetadata `json:"auth,omitempty"`
|
|
Annotations *ToolAnnotations `json:"annotations,omitempty"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
FlagHints map[string]CLIFlagHint `json:"flag_hints,omitempty"`
|
|
FlagOverlay map[string]FlagOverlay `json:"flag_overlay,omitempty"`
|
|
SourceServerKey string `json:"source_server_key"`
|
|
CanonicalPath string `json:"canonical_path"`
|
|
}
|
|
|
|
type ToolAuthMetadata struct {
|
|
Version string `json:"version,omitempty"`
|
|
ProductCode string `json:"productCode,omitempty"`
|
|
Domain string `json:"domain,omitempty"`
|
|
ClientObservedScopes []string `json:"clientObservedScopes,omitempty"`
|
|
RequiredScopes []string `json:"requiredScopes,omitempty"`
|
|
RequiredPermissions []string `json:"requiredPermissions,omitempty"`
|
|
RecommendedScopes []string `json:"recommendedScopes,omitempty"`
|
|
ExcludedScopes []string `json:"excludedScopes,omitempty"`
|
|
GrantProductCodes []string `json:"grantProductCodes,omitempty"`
|
|
RiskHint string `json:"riskHint,omitempty"`
|
|
RiskAction string `json:"riskAction,omitempty"`
|
|
ConfirmationRequired bool `json:"confirmationRequired,omitempty"`
|
|
Identities []string `json:"identities,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
AuthMetaVersion string `json:"authMetaVersion,omitempty"`
|
|
AuthMetaHash string `json:"authMetaHash,omitempty"`
|
|
}
|
|
|
|
func BuildCatalog(runtimeServers []discovery.RuntimeServer) Catalog {
|
|
sorted := append([]discovery.RuntimeServer(nil), runtimeServers...)
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
left := sorted[i].Server
|
|
right := sorted[j].Server
|
|
if left.DisplayName != right.DisplayName {
|
|
return left.DisplayName < right.DisplayName
|
|
}
|
|
if left.Endpoint != right.Endpoint {
|
|
return left.Endpoint < right.Endpoint
|
|
}
|
|
return left.Key < right.Key
|
|
})
|
|
|
|
usedIDs := make(map[string]struct{}, len(sorted))
|
|
products := make([]CanonicalProduct, 0, len(sorted))
|
|
for _, runtimeServer := range sorted {
|
|
productID := nextCanonicalProductID(
|
|
runtimeServer.Server.Key,
|
|
runtimeServer.Server.DisplayName,
|
|
runtimeServer.Server.Endpoint,
|
|
runtimeServer.Server.CLI.Command,
|
|
usedIDs,
|
|
)
|
|
sensitiveByTool := make(map[string]bool, len(runtimeServer.Server.CLI.Tools))
|
|
hasSensitiveOverride := make(map[string]bool, len(runtimeServer.Server.CLI.Tools))
|
|
toolCLIName := make(map[string]string, len(runtimeServer.Server.CLI.Tools))
|
|
toolGroup := make(map[string]string)
|
|
toolHidden := make(map[string]bool, len(runtimeServer.Server.CLI.Tools))
|
|
toolTitleOverride := make(map[string]string, len(runtimeServer.Server.CLI.Tools))
|
|
toolDescriptionOverride := make(map[string]string, len(runtimeServer.Server.CLI.Tools))
|
|
toolFlagHints := make(map[string]map[string]CLIFlagHint, len(runtimeServer.Server.CLI.Tools))
|
|
toolFlagOverlay := make(map[string]map[string]FlagOverlay)
|
|
|
|
// Merge richer market.CLIToolOverride data (group, cliName, flag
|
|
// transforms/aliases/env-defaults) that the dynamic compat layer
|
|
// already consumes but the IR previously dropped.
|
|
for name, override := range runtimeServer.Server.CLI.ToolOverrides {
|
|
trimmed := strings.TrimSpace(name)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if cn := strings.TrimSpace(override.CLIName); cn != "" {
|
|
toolCLIName[trimmed] = cn
|
|
}
|
|
if gp := strings.TrimSpace(override.Group); gp != "" {
|
|
toolGroup[trimmed] = gp
|
|
}
|
|
if override.Hidden {
|
|
toolHidden[trimmed] = true
|
|
}
|
|
if override.IsSensitive {
|
|
sensitiveByTool[trimmed] = true
|
|
hasSensitiveOverride[trimmed] = true
|
|
}
|
|
if desc := strings.TrimSpace(override.Description); desc != "" && toolDescriptionOverride[trimmed] == "" {
|
|
toolDescriptionOverride[trimmed] = desc
|
|
}
|
|
if len(override.Flags) > 0 {
|
|
overlay := make(map[string]FlagOverlay, len(override.Flags))
|
|
for paramName, flagOverride := range override.Flags {
|
|
paramTrim := strings.TrimSpace(paramName)
|
|
if paramTrim == "" {
|
|
continue
|
|
}
|
|
overlay[paramTrim] = FlagOverlay{
|
|
Alias: strings.TrimSpace(flagOverride.Alias),
|
|
Transform: strings.TrimSpace(flagOverride.Transform),
|
|
TransformArgs: cloneTransformArgs(flagOverride.TransformArgs),
|
|
EnvDefault: strings.TrimSpace(flagOverride.EnvDefault),
|
|
Default: strings.TrimSpace(flagOverride.Default),
|
|
Hidden: flagOverride.Hidden,
|
|
}
|
|
}
|
|
if len(overlay) > 0 {
|
|
toolFlagOverlay[trimmed] = overlay
|
|
}
|
|
}
|
|
}
|
|
for _, cliTool := range runtimeServer.Server.CLI.Tools {
|
|
name := strings.TrimSpace(cliTool.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
sensitiveByTool[name] = cliTool.IsSensitive
|
|
hasSensitiveOverride[name] = true
|
|
toolCLIName[name] = strings.TrimSpace(cliTool.CLIName)
|
|
toolHidden[name] = cliTool.Hidden
|
|
toolTitleOverride[name] = strings.TrimSpace(cliTool.Title)
|
|
toolDescriptionOverride[name] = strings.TrimSpace(cliTool.Description)
|
|
if len(cliTool.Flags) > 0 {
|
|
hints := make(map[string]CLIFlagHint, len(cliTool.Flags))
|
|
for key, hint := range cliTool.Flags {
|
|
trimmed := strings.TrimSpace(key)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
hints[trimmed] = CLIFlagHint{
|
|
Shorthand: strings.TrimSpace(hint.Shorthand),
|
|
Alias: strings.TrimSpace(hint.Alias),
|
|
}
|
|
}
|
|
if len(hints) > 0 {
|
|
toolFlagHints[name] = hints
|
|
}
|
|
}
|
|
}
|
|
tools := make([]ToolDescriptor, 0, len(runtimeServer.Tools))
|
|
for _, tool := range runtimeServer.Tools {
|
|
title := strings.TrimSpace(tool.Title)
|
|
if override := strings.TrimSpace(toolTitleOverride[tool.Name]); override != "" {
|
|
title = override
|
|
}
|
|
if title == "" {
|
|
title = tool.Name
|
|
}
|
|
description := strings.TrimSpace(tool.Description)
|
|
if override := strings.TrimSpace(toolDescriptionOverride[tool.Name]); override != "" {
|
|
description = override
|
|
}
|
|
if description == "" {
|
|
description = title
|
|
}
|
|
sensitive := tool.Sensitive
|
|
if hasSensitiveOverride[tool.Name] {
|
|
sensitive = sensitiveByTool[tool.Name]
|
|
}
|
|
cliName := strings.TrimSpace(toolCLIName[tool.Name])
|
|
if cliName == "" {
|
|
cliName = tool.Name
|
|
}
|
|
inputSchema := cloneMap(tool.InputSchema)
|
|
tools = append(tools, ToolDescriptor{
|
|
RPCName: tool.Name,
|
|
CLIName: cliName,
|
|
Group: strings.TrimSpace(toolGroup[tool.Name]),
|
|
Title: title,
|
|
Description: description,
|
|
InputSchema: inputSchema,
|
|
OutputSchema: cloneMap(tool.OutputSchema),
|
|
Sensitive: sensitive,
|
|
Auth: extractToolAuthMetadata(inputSchema, productID),
|
|
Annotations: deriveAnnotations(sensitive),
|
|
Hidden: toolHidden[tool.Name],
|
|
FlagHints: cloneFlagHints(toolFlagHints[tool.Name]),
|
|
FlagOverlay: cloneFlagOverlay(toolFlagOverlay[tool.Name]),
|
|
SourceServerKey: runtimeServer.Server.Key,
|
|
CanonicalPath: fmt.Sprintf("%s.%s", productID, tool.Name),
|
|
})
|
|
}
|
|
sort.Slice(tools, func(i, j int) bool {
|
|
return tools[i].RPCName < tools[j].RPCName
|
|
})
|
|
|
|
lifecycle := LifecycleInfo{
|
|
DeprecatedBy: runtimeServer.Server.Lifecycle.DeprecatedBy,
|
|
DeprecationDate: strings.TrimSpace(runtimeServer.Server.Lifecycle.DeprecationDate),
|
|
MigrationURL: strings.TrimSpace(runtimeServer.Server.Lifecycle.MigrationURL),
|
|
DeprecatedCandidate: runtimeServer.Server.Lifecycle.DeprecatedCandidate,
|
|
}
|
|
var lifecyclePtr *LifecycleInfo
|
|
if lifecycle.DeprecatedBy > 0 || lifecycle.DeprecationDate != "" || lifecycle.MigrationURL != "" || lifecycle.DeprecatedCandidate {
|
|
lifecycleCopy := lifecycle
|
|
lifecyclePtr = &lifecycleCopy
|
|
}
|
|
cliMeta := ProductCLIMetadata{
|
|
Command: strings.TrimSpace(runtimeServer.Server.CLI.Command),
|
|
Group: strings.TrimSpace(runtimeServer.Server.CLI.Group),
|
|
Hidden: runtimeServer.Server.CLI.Hidden,
|
|
Skip: runtimeServer.Server.CLI.Skip,
|
|
Aliases: append([]string(nil), runtimeServer.Server.CLI.Aliases...),
|
|
Prefixes: append([]string(nil), runtimeServer.Server.CLI.Prefixes...),
|
|
}
|
|
var cliPtr *ProductCLIMetadata
|
|
if cliMeta.Command != "" || cliMeta.Group != "" || cliMeta.Hidden || cliMeta.Skip || len(cliMeta.Aliases) > 0 || len(cliMeta.Prefixes) > 0 {
|
|
cliCopy := cliMeta
|
|
cliPtr = &cliCopy
|
|
}
|
|
|
|
description := runtimeServer.Server.Description
|
|
if cliDescription := strings.TrimSpace(runtimeServer.Server.CLI.Description); cliDescription != "" {
|
|
description = cliDescription
|
|
}
|
|
|
|
products = append(products, CanonicalProduct{
|
|
ID: productID,
|
|
DisplayName: runtimeServer.Server.DisplayName,
|
|
Description: description,
|
|
ServerKey: runtimeServer.Server.Key,
|
|
Endpoint: runtimeServer.Server.Endpoint,
|
|
SchemaURI: runtimeServer.Server.SchemaURI,
|
|
NegotiatedProtocolVersion: runtimeServer.NegotiatedProtocolVersion,
|
|
Source: runtimeServer.Source,
|
|
Degraded: runtimeServer.Degraded,
|
|
Lifecycle: lifecyclePtr,
|
|
CLI: cliPtr,
|
|
Tools: tools,
|
|
})
|
|
}
|
|
|
|
return Catalog{Products: products}
|
|
}
|
|
|
|
func extractToolAuthMetadata(schema map[string]any, productID string) *ToolAuthMetadata {
|
|
raw := any(nil)
|
|
if len(schema) > 0 {
|
|
raw = schema["x-dingtalk-auth"]
|
|
}
|
|
if raw == nil {
|
|
return productGrantAuthMetadata(productID)
|
|
}
|
|
data, err := json.Marshal(raw)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var metadata ToolAuthMetadata
|
|
if err := json.Unmarshal(data, &metadata); err != nil {
|
|
return nil
|
|
}
|
|
if !hasMeaningfulToolAuthMetadata(metadata) {
|
|
return nil
|
|
}
|
|
return &metadata
|
|
}
|
|
|
|
func hasMeaningfulToolAuthMetadata(metadata ToolAuthMetadata) bool {
|
|
return strings.TrimSpace(metadata.Version) != "" ||
|
|
strings.TrimSpace(metadata.ProductCode) != "" ||
|
|
strings.TrimSpace(metadata.Domain) != "" ||
|
|
len(metadata.ClientObservedScopes) > 0 ||
|
|
len(metadata.RequiredScopes) > 0 ||
|
|
len(metadata.RequiredPermissions) > 0 ||
|
|
len(metadata.RecommendedScopes) > 0 ||
|
|
len(metadata.ExcludedScopes) > 0 ||
|
|
len(metadata.GrantProductCodes) > 0 ||
|
|
strings.TrimSpace(metadata.RiskHint) != "" ||
|
|
strings.TrimSpace(metadata.RiskAction) != "" ||
|
|
metadata.ConfirmationRequired ||
|
|
len(metadata.Identities) > 0 ||
|
|
strings.TrimSpace(metadata.Source) != "" ||
|
|
strings.TrimSpace(metadata.AuthMetaVersion) != "" ||
|
|
strings.TrimSpace(metadata.AuthMetaHash) != ""
|
|
}
|
|
|
|
func productGrantAuthMetadata(productID string) *ToolAuthMetadata {
|
|
productID = strings.TrimSpace(productID)
|
|
if productID == "" {
|
|
return nil
|
|
}
|
|
metadata := ToolAuthMetadata{
|
|
Version: "v1",
|
|
ProductCode: productID,
|
|
Domain: productID,
|
|
GrantProductCodes: []string{
|
|
productID,
|
|
},
|
|
Source: "dws-product-fallback",
|
|
AuthMetaVersion: "v1",
|
|
}
|
|
metadata.AuthMetaHash = toolAuthMetaHash(metadata)
|
|
return &metadata
|
|
}
|
|
|
|
func toolAuthMetaHash(metadata ToolAuthMetadata) string {
|
|
metadata.AuthMetaHash = ""
|
|
data, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (c Catalog) FindProduct(id string) (CanonicalProduct, bool) {
|
|
for _, product := range c.Products {
|
|
if product.ID == id {
|
|
return product, true
|
|
}
|
|
}
|
|
return CanonicalProduct{}, false
|
|
}
|
|
|
|
func (c Catalog) FindTool(path string) (CanonicalProduct, ToolDescriptor, bool) {
|
|
productID, toolName, ok := strings.Cut(path, ".")
|
|
if !ok || productID == "" || toolName == "" {
|
|
return CanonicalProduct{}, ToolDescriptor{}, false
|
|
}
|
|
product, ok := c.FindProduct(productID)
|
|
if !ok {
|
|
return CanonicalProduct{}, ToolDescriptor{}, false
|
|
}
|
|
tool, ok := product.FindTool(toolName)
|
|
if !ok {
|
|
return CanonicalProduct{}, ToolDescriptor{}, false
|
|
}
|
|
return product, tool, true
|
|
}
|
|
|
|
func (p CanonicalProduct) FindTool(name string) (ToolDescriptor, bool) {
|
|
for _, tool := range p.Tools {
|
|
if tool.RPCName == name {
|
|
return tool, true
|
|
}
|
|
}
|
|
return ToolDescriptor{}, false
|
|
}
|
|
|
|
func nextCanonicalProductID(key, displayName, endpoint, cliCommand string, usedIDs map[string]struct{}) string {
|
|
base := canonicalProductID(key, displayName, endpoint, cliCommand)
|
|
id := base
|
|
if _, exists := usedIDs[id]; exists {
|
|
id = fmt.Sprintf("%s-%s", base, shorten(key))
|
|
}
|
|
usedIDs[id] = struct{}{}
|
|
return id
|
|
}
|
|
|
|
func canonicalProductID(key, displayName, endpoint, cliCommand string) string {
|
|
for _, candidate := range []string{
|
|
slugify(cliCommand),
|
|
endpointSlug(endpoint),
|
|
slugify(displayName),
|
|
slugify(key),
|
|
} {
|
|
if candidate != "" {
|
|
return canonicalProductAlias(candidate)
|
|
}
|
|
}
|
|
return "srv-" + shorten(key)
|
|
}
|
|
|
|
func canonicalProductAlias(id string) string {
|
|
switch id {
|
|
case "table":
|
|
return "aitable"
|
|
default:
|
|
return id
|
|
}
|
|
}
|
|
|
|
func endpointSlug(raw string) string {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
path := strings.Trim(parsed.Path, "/")
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
parts := strings.Split(path, "/")
|
|
return slugify(parts[len(parts)-1])
|
|
}
|
|
|
|
func slugify(value string) string {
|
|
var builder strings.Builder
|
|
lastDash := false
|
|
for _, r := range strings.ToLower(strings.TrimSpace(value)) {
|
|
switch {
|
|
case r >= 'a' && r <= 'z':
|
|
builder.WriteRune(r)
|
|
lastDash = false
|
|
case r >= '0' && r <= '9':
|
|
builder.WriteRune(r)
|
|
lastDash = false
|
|
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
|
continue
|
|
default:
|
|
if !lastDash && builder.Len() > 0 {
|
|
builder.WriteByte('-')
|
|
lastDash = true
|
|
}
|
|
}
|
|
}
|
|
return strings.Trim(builder.String(), "-")
|
|
}
|
|
|
|
func shorten(value string) string {
|
|
if strings.TrimSpace(value) == "" {
|
|
sum := sha256.Sum256([]byte("dws"))
|
|
return hex.EncodeToString(sum[:])[:8]
|
|
}
|
|
sum := sha256.Sum256([]byte(value))
|
|
return hex.EncodeToString(sum[:])[:8]
|
|
}
|
|
|
|
func cloneMap(value map[string]any) map[string]any {
|
|
if len(value) == 0 {
|
|
return nil
|
|
}
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var cloned map[string]any
|
|
if err := json.Unmarshal(data, &cloned); err != nil {
|
|
return nil
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func cloneFlagHints(value map[string]CLIFlagHint) map[string]CLIFlagHint {
|
|
if len(value) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]CLIFlagHint, len(value))
|
|
for key, hint := range value {
|
|
out[key] = hint
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneFlagOverlay(value map[string]FlagOverlay) map[string]FlagOverlay {
|
|
if len(value) == 0 {
|
|
return nil
|
|
}
|
|
out := make(map[string]FlagOverlay, len(value))
|
|
for key, overlay := range value {
|
|
overlay.TransformArgs = cloneTransformArgs(overlay.TransformArgs)
|
|
out[key] = overlay
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneTransformArgs(value map[string]any) map[string]any {
|
|
if len(value) == 0 {
|
|
return nil
|
|
}
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var cloned map[string]any
|
|
if err := json.Unmarshal(data, &cloned); err != nil {
|
|
return nil
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
// deriveAnnotations maps the coarse-grained Sensitive bool onto MCP 2025+
|
|
// annotation hints. Only DestructiveHint is populated — the other hints
|
|
// (read_only, idempotent, open_world) stay nil until the upstream tool
|
|
// manifest advertises them explicitly. Guessing from name prefixes
|
|
// ("list_", "get_") risks false signals for AI agents.
|
|
func deriveAnnotations(sensitive bool) *ToolAnnotations {
|
|
if !sensitive {
|
|
return nil
|
|
}
|
|
destructive := true
|
|
return &ToolAnnotations{DestructiveHint: &destructive}
|
|
}
|