From d4a1dbee727d9061ac5f4db81346e6c7402677e5 Mon Sep 17 00:00:00 2001 From: John Miller Date: Thu, 2 Apr 2026 21:42:30 -0400 Subject: [PATCH 1/2] feat: Add ToolboxTool and ToolboxResource implementations - Introduced ToolboxTool struct with methods for JSON and YAML serialization. - Added ToolboxResource struct to manage collections of ToolboxTools. - Implemented loading and saving functionalities for both ToolboxTool and ToolboxResource. - Created unit tests for ToolboxTool and ToolboxResource to ensure correct functionality. - Added JSON schema definitions for ToolboxTool and ToolboxResource. --- agentschema-emitter/lib/model/manifest.tsp | 78 +++++- agentschema-emitter/src/csharp.ts | 4 +- agentschema-emitter/src/typescript.ts | 2 +- .../content/docs/reference/AgentManifest.md | 2 +- docs/src/content/docs/reference/README.md | 16 ++ docs/src/content/docs/reference/Resource.md | 7 + .../content/docs/reference/ToolboxResource.md | 71 +++++ .../src/content/docs/reference/ToolboxTool.md | 52 ++++ examples/toolbox/toolbox_manifest.yaml | 63 +++++ .../ToolboxResourceConversionTests.cs | 201 +++++++++++++++ .../ToolboxToolConversionTests.cs | 158 ++++++++++++ runtime/csharp/AgentSchema/Resource.cs | 1 + runtime/csharp/AgentSchema/ToolboxResource.cs | 243 ++++++++++++++++++ runtime/csharp/AgentSchema/ToolboxTool.cs | 220 ++++++++++++++++ runtime/go/agentschema/resource.go | 100 +++++++ .../go/agentschema/toolbox_resource_test.go | 231 +++++++++++++++++ runtime/go/agentschema/toolbox_tool.go | 114 ++++++++ runtime/go/agentschema/toolbox_tool_test.go | 196 ++++++++++++++ .../agentschema/src/agentschema/_Resource.py | 155 +++++++++++ .../src/agentschema/_ToolboxTool.py | 127 +++++++++ .../agentschema/src/agentschema/__init__.py | 7 +- .../tests/test_toolbox_resource.py | 157 +++++++++++ .../agentschema/tests/test_toolbox_tool.py | 110 ++++++++ runtime/rust/agentschema/src/lib.rs | 3 + runtime/rust/agentschema/src/resource.rs | 111 ++++++++ runtime/rust/agentschema/src/toolbox_tool.rs | 101 ++++++++ .../tests/toolbox_resource_test.rs | 112 ++++++++ .../agentschema/tests/toolbox_tool_test.rs | 89 +++++++ runtime/typescript/agentschema/src/index.ts | 4 +- .../typescript/agentschema/src/resource.ts | 209 +++++++++++++++ .../agentschema/src/toolbox-tool.ts | 196 ++++++++++++++ .../tests/toolbox-resource.test.ts | 79 ++++++ .../agentschema/tests/toolbox-tool.test.ts | 95 +++++++ schemas/v1.0/ToolboxResource.yaml | 24 ++ schemas/v1.0/ToolboxTool.yaml | 50 ++++ 35 files changed, 3381 insertions(+), 7 deletions(-) create mode 100644 docs/src/content/docs/reference/ToolboxResource.md create mode 100644 docs/src/content/docs/reference/ToolboxTool.md create mode 100644 examples/toolbox/toolbox_manifest.yaml create mode 100644 runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs create mode 100644 runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs create mode 100644 runtime/csharp/AgentSchema/ToolboxResource.cs create mode 100644 runtime/csharp/AgentSchema/ToolboxTool.cs create mode 100644 runtime/go/agentschema/toolbox_resource_test.go create mode 100644 runtime/go/agentschema/toolbox_tool.go create mode 100644 runtime/go/agentschema/toolbox_tool_test.go create mode 100644 runtime/python/agentschema/src/agentschema/_ToolboxTool.py create mode 100644 runtime/python/agentschema/tests/test_toolbox_resource.py create mode 100644 runtime/python/agentschema/tests/test_toolbox_tool.py create mode 100644 runtime/rust/agentschema/src/toolbox_tool.rs create mode 100644 runtime/rust/agentschema/tests/toolbox_resource_test.rs create mode 100644 runtime/rust/agentschema/tests/toolbox_tool_test.rs create mode 100644 runtime/typescript/agentschema/src/toolbox-tool.ts create mode 100644 runtime/typescript/agentschema/tests/toolbox-resource.test.ts create mode 100644 runtime/typescript/agentschema/tests/toolbox-tool.test.ts create mode 100644 schemas/v1.0/ToolboxResource.yaml create mode 100644 schemas/v1.0/ToolboxTool.yaml diff --git a/agentschema-emitter/lib/model/manifest.tsp b/agentschema-emitter/lib/model/manifest.tsp index 5c31bc98..082c9452 100644 --- a/agentschema-emitter/lib/model/manifest.tsp +++ b/agentschema-emitter/lib/model/manifest.tsp @@ -90,7 +90,7 @@ model AgentManifest { resources: Resources; } -alias ResourceKind = "model" | "tool"; +alias ResourceKind = "model" | "tool" | "toolbox"; /** * Represents a resource required by the agent * Resources can include databases, APIs, or other external systems @@ -139,3 +139,79 @@ alias Resources = Record | Named< "Name of the resource", #{ name: "my-resource" } >[]; + +alias ToolboxToolId = + | "bing_grounding" + | "azure_ai_search" + | "code_interpreter" + | "file_search" + | "mcp" + | "openapi" + | string; + +alias ToolboxAuthType = + | "CustomKeys" + | "OAuth2" + | "UserEntraToken" + | "ProjectManagedIdentity" + | "AgenticIdentityToken" + | string; + +/** + * Represents a tool definition within a toolbox. + * Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) + * or external (mcp, openapi) with connection details. + */ +model ToolboxTool { + @doc("The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp')") + @sample(#{ id: "bing_grounding" }) + id: ToolboxToolId; + + @doc("Optional display name for the tool") + @sample(#{ name: "my-search-tool" }) + name?: string; + + @doc("Target endpoint URL for external tools (e.g., MCP server URL)") + @sample(#{ target: "https://api.githubcopilot.com/mcp" }) + target?: string; + + @doc("Authentication type for the tool connection") + @sample(#{ authType: "OAuth2" }) + authType?: ToolboxAuthType; + + @doc("Additional configuration options for the tool") + @sample(#{ options: #{ indexName: "products-index" } }) + options?: Record = #{}; +} + +/** + * Represents a Foundry Toolbox resource — a named collection of tools + * that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. + */ +model ToolboxResource extends Resource { + @doc("The kind identifier for toolbox resources") + @sample(#{ kind: "toolbox" }) + kind: "toolbox"; + + @doc("Description of the toolbox") + @sample(#{ description: "Shared platform tools" }) + description?: string; + + @doc("The tools contained in this toolbox") + @sample(#{ + tools: #[ + #{ id: "bing_grounding" }, + #{ + id: "azure_ai_search", + options: #{ indexName: "products-index" }, + }, + #{ + id: "mcp", + name: "github-copilot", + target: "https://api.githubcopilot.com/mcp", + authType: "OAuth2", + } + ], + }) + tools: ToolboxTool[]; +} diff --git a/agentschema-emitter/src/csharp.ts b/agentschema-emitter/src/csharp.ts index b3ad869e..a15cc51d 100644 --- a/agentschema-emitter/src/csharp.ts +++ b/agentschema-emitter/src/csharp.ts @@ -145,8 +145,8 @@ const renderCSharp = (nodes: TypeNode[], node: TypeNode, classTemplate: nunjucks let hasNameProperty = false; if (itemType) { - // Check if item type has a 'name' property (supports object format) - hasNameProperty = itemType.properties.some(prop => prop.name === "name"); + // Check if item type has a required 'name' property (supports object format) + hasNameProperty = itemType.properties.some(prop => prop.name === "name" && !prop.isOptional); if (itemType.alternates && itemType.alternates.length > 0) { const firstAlt = itemType.alternates[0]; diff --git a/agentschema-emitter/src/typescript.ts b/agentschema-emitter/src/typescript.ts index 39c4ea78..2e063d45 100644 --- a/agentschema-emitter/src/typescript.ts +++ b/agentschema-emitter/src/typescript.ts @@ -318,7 +318,7 @@ function getCollectionTypes(node: TypeNode): Array<{ prop: PropertyNode; type: s .map((p) => ({ prop: p, type: p.type?.properties.filter((t) => t.name !== "name").map((t) => t.name) || [], - hasNameProperty: p.type?.properties.some((t) => t.name === "name") || false, + hasNameProperty: p.type?.properties.some((t) => t.name === "name" && !t.isOptional) || false, })); } diff --git a/docs/src/content/docs/reference/AgentManifest.md b/docs/src/content/docs/reference/AgentManifest.md index 9988db7e..3816eb37 100644 --- a/docs/src/content/docs/reference/AgentManifest.md +++ b/docs/src/content/docs/reference/AgentManifest.md @@ -103,7 +103,7 @@ resources: | metadata | dictionary | Additional metadata including authors, tags, and other arbitrary properties | | template | [AgentDefinition](../agentdefinition/) | The agent that this manifest is based on(Related Types: [PromptAgent](../promptagent/), [Workflow](../workflow/), [ContainerAgent](../containeragent/)) | | parameters | [PropertySchema](../propertyschema/) | Parameters for configuring the agent's behavior and execution | -| resources | [Resource[]](../resource/) | Resources required by the agent, such as models or tools(Related Types: [ModelResource](../modelresource/), [ToolResource](../toolresource/)) | +| resources | [Resource[]](../resource/) | Resources required by the agent, such as models or tools(Related Types: [ModelResource](../modelresource/), [ToolResource](../toolresource/), [ToolboxResource](../toolboxresource/)) | ## Composed Types diff --git a/docs/src/content/docs/reference/README.md b/docs/src/content/docs/reference/README.md index d52403dc..9f7103ec 100644 --- a/docs/src/content/docs/reference/README.md +++ b/docs/src/content/docs/reference/README.md @@ -269,6 +269,20 @@ classDiagram +string id +dictionary options } + class ToolboxTool { + + +string id + +string name + +string target + +string authType + +dictionary options + } + class ToolboxResource { + + +string kind + +string description + +ToolboxTool[] tools + } class AgentManifest { +string name @@ -302,6 +316,7 @@ classDiagram McpServerApprovalMode <|-- McpServerToolSpecifyApprovalMode Resource <|-- ModelResource Resource <|-- ToolResource + Resource <|-- ToolboxResource AgentDefinition *-- PropertySchema AgentDefinition *-- PropertySchema Model *-- Connection @@ -326,6 +341,7 @@ classDiagram ContainerAgent *-- ProtocolVersionRecord ContainerAgent *-- ContainerResources ContainerAgent *-- EnvironmentVariable + ToolboxResource *-- ToolboxTool AgentManifest *-- AgentDefinition AgentManifest *-- PropertySchema AgentManifest *-- Resource diff --git a/docs/src/content/docs/reference/Resource.md b/docs/src/content/docs/reference/Resource.md index 109649ba..14c6a081 100644 --- a/docs/src/content/docs/reference/Resource.md +++ b/docs/src/content/docs/reference/Resource.md @@ -36,6 +36,12 @@ classDiagram +dictionary options } Resource <|-- ToolResource + class ToolboxResource { + +string kind + +string description + +ToolboxTool[] tools + } + Resource <|-- ToolboxResource ``` ## Yaml Example @@ -58,3 +64,4 @@ The following types extend `Resource`: - [ModelResource](../modelresource/) - [ToolResource](../toolresource/) +- [ToolboxResource](../toolboxresource/) diff --git a/docs/src/content/docs/reference/ToolboxResource.md b/docs/src/content/docs/reference/ToolboxResource.md new file mode 100644 index 00000000..2617c3f7 --- /dev/null +++ b/docs/src/content/docs/reference/ToolboxResource.md @@ -0,0 +1,71 @@ +--- +title: "ToolboxResource" +description: "Documentation for the ToolboxResource type." +slug: "reference/toolboxresource" +--- + +Represents a Foundry Toolbox resource — a named collection of tools +that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. + +## Class Diagram + +```mermaid +--- +title: ToolboxResource +config: + look: handDrawn + theme: colorful + class: + hideEmptyMembersBox: true +--- +classDiagram + class Resource { + +string name + +string kind + } + Resource <|-- ToolboxResource + class ToolboxResource { + + +string kind + +string description + +ToolboxTool[] tools + } + class ToolboxTool { + +string id + +string name + +string target + +string authType + +dictionary options + } + ToolboxResource *-- ToolboxTool +``` + +## Yaml Example + +```yaml +kind: toolbox +description: Shared platform tools +tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: https://api.githubcopilot.com/mcp + authType: OAuth2 +``` + +## Properties + +| Name | Type | Description | +| ---- | ---- | ----------- | +| kind | string | The kind identifier for toolbox resources | +| description | string | Description of the toolbox | +| tools | [ToolboxTool[]](../toolboxtool/) | The tools contained in this toolbox | + +## Composed Types + +The following types are composed within `ToolboxResource`: + +- [ToolboxTool](../toolboxtool/) diff --git a/docs/src/content/docs/reference/ToolboxTool.md b/docs/src/content/docs/reference/ToolboxTool.md new file mode 100644 index 00000000..89e395e8 --- /dev/null +++ b/docs/src/content/docs/reference/ToolboxTool.md @@ -0,0 +1,52 @@ +--- +title: "ToolboxTool" +description: "Documentation for the ToolboxTool type." +slug: "reference/toolboxtool" +--- + +Represents a tool definition within a toolbox. +Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) +or external (mcp, openapi) with connection details. + +## Class Diagram + +```mermaid +--- +title: ToolboxTool +config: + look: handDrawn + theme: colorful + class: + hideEmptyMembersBox: true +--- +classDiagram + class ToolboxTool { + + +string id + +string name + +string target + +string authType + +dictionary options + } +``` + +## Yaml Example + +```yaml +id: bing_grounding +name: my-search-tool +target: https://api.githubcopilot.com/mcp +authType: OAuth2 +options: + indexName: products-index +``` + +## Properties + +| Name | Type | Description | +| ---- | ---- | ----------- | +| id | string | The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') | +| name | string | Optional display name for the tool | +| target | string | Target endpoint URL for external tools (e.g., MCP server URL) | +| authType | string | Authentication type for the tool connection | +| options | dictionary | Additional configuration options for the tool | diff --git a/examples/toolbox/toolbox_manifest.yaml b/examples/toolbox/toolbox_manifest.yaml new file mode 100644 index 00000000..9bcef1af --- /dev/null +++ b/examples/toolbox/toolbox_manifest.yaml @@ -0,0 +1,63 @@ +template: + name: Platform Support Agent + description: |- + A hosted container agent that uses a Foundry Toolbox with Bing search, + Azure AI Search, and a custom MCP server for GitHub Copilot integration. + metadata: + tags: + - example + - toolbox + authors: + - sethjuarez + + kind: hosted + image: myregistry.azurecr.io/support-agent + protocols: + - protocol: a2a + version: v0.1.0 + resources: + cpu: "1" + memory: "2Gi" + +resources: + - kind: model + name: chat + id: {{ model_name }} + + - kind: toolbox + name: platform-tools + description: Shared platform tools for support workflows + tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: support-docs-index + queryType: vector_semantic_hybrid + - id: code_interpreter + - id: mcp + name: github-copilot + target: https://api.githubcopilot.com/mcp + authType: OAuth2 + options: + clientId: "{{ github_client_id }}" + clientSecret: "{{ github_client_secret }}" + +parameters: + model_name: + schema: + type: string + enum: + - gpt-4o + - gpt-4o-mini + default: gpt-4o + required: true + github_client_id: + schema: + type: string + description: OAuth client ID for GitHub Copilot MCP server + required: true + github_client_secret: + schema: + type: string + description: OAuth client secret for GitHub Copilot MCP server + required: true diff --git a/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs b/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs new file mode 100644 index 00000000..36bae104 --- /dev/null +++ b/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs @@ -0,0 +1,201 @@ + +using Xunit; + +#pragma warning disable IDE0130 +namespace AgentSchema; +#pragma warning restore IDE0130 + + +public class ToolboxResourceConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +kind: toolbox +description: Shared platform tools +tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + +"""; + + var instance = ToolboxResource.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("toolbox", instance.Kind); + Assert.Equal("Shared platform tools", instance.Description); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +"""; + + var instance = ToolboxResource.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("toolbox", instance.Kind); + Assert.Equal("Shared platform tools", instance.Description); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +"""; + + var original = ToolboxResource.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolboxResource.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("toolbox", reloaded.Kind); + Assert.Equal("Shared platform tools", reloaded.Description); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +kind: toolbox +description: Shared platform tools +tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + +"""; + + var original = ToolboxResource.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolboxResource.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("toolbox", reloaded.Kind); + Assert.Equal("Shared platform tools", reloaded.Description); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +"""; + + var instance = ToolboxResource.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +kind: toolbox +description: Shared platform tools +tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + +"""; + + var instance = ToolboxResource.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs b/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs new file mode 100644 index 00000000..6e8883ad --- /dev/null +++ b/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs @@ -0,0 +1,158 @@ + +using Xunit; + +#pragma warning disable IDE0130 +namespace AgentSchema; +#pragma warning restore IDE0130 + + +public class ToolboxToolConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +id: bing_grounding +name: my-search-tool +target: "https://api.githubcopilot.com/mcp" +authType: OAuth2 +options: + indexName: products-index + +"""; + + var instance = ToolboxTool.FromYaml(yamlData); + + Assert.NotNull(instance); + Assert.Equal("bing_grounding", instance.Id); + Assert.Equal("my-search-tool", instance.Name); + Assert.Equal("https://api.githubcopilot.com/mcp", instance.Target); + Assert.Equal("OAuth2", instance.AuthType); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +"""; + + var instance = ToolboxTool.FromJson(jsonData); + Assert.NotNull(instance); + Assert.Equal("bing_grounding", instance.Id); + Assert.Equal("my-search-tool", instance.Name); + Assert.Equal("https://api.githubcopilot.com/mcp", instance.Target); + Assert.Equal("OAuth2", instance.AuthType); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +"""; + + var original = ToolboxTool.FromJson(jsonData); + Assert.NotNull(original); + + var json = original.ToJson(); + Assert.False(string.IsNullOrEmpty(json)); + + var reloaded = ToolboxTool.FromJson(json); + Assert.NotNull(reloaded); + Assert.Equal("bing_grounding", reloaded.Id); + Assert.Equal("my-search-tool", reloaded.Name); + Assert.Equal("https://api.githubcopilot.com/mcp", reloaded.Target); + Assert.Equal("OAuth2", reloaded.AuthType); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +id: bing_grounding +name: my-search-tool +target: "https://api.githubcopilot.com/mcp" +authType: OAuth2 +options: + indexName: products-index + +"""; + + var original = ToolboxTool.FromYaml(yamlData); + Assert.NotNull(original); + + var yaml = original.ToYaml(); + Assert.False(string.IsNullOrEmpty(yaml)); + + var reloaded = ToolboxTool.FromYaml(yaml); + Assert.NotNull(reloaded); + Assert.Equal("bing_grounding", reloaded.Id); + Assert.Equal("my-search-tool", reloaded.Name); + Assert.Equal("https://api.githubcopilot.com/mcp", reloaded.Target); + Assert.Equal("OAuth2", reloaded.AuthType); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +"""; + + var instance = ToolboxTool.FromJson(jsonData); + var json = instance.ToJson(); + + // Verify it's valid JSON by parsing it + var parsed = System.Text.Json.JsonDocument.Parse(json); + Assert.NotNull(parsed); + } + + [Fact] + public void ToYamlProducesValidYaml() + { + string yamlData = """ +id: bing_grounding +name: my-search-tool +target: "https://api.githubcopilot.com/mcp" +authType: OAuth2 +options: + indexName: products-index + +"""; + + var instance = ToolboxTool.FromYaml(yamlData); + var yaml = instance.ToYaml(); + + // Verify it's valid YAML by parsing it + var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build(); + var parsed = deserializer.Deserialize(yaml); + Assert.NotNull(parsed); + } +} diff --git a/runtime/csharp/AgentSchema/Resource.cs b/runtime/csharp/AgentSchema/Resource.cs index b88556ff..70be3044 100644 --- a/runtime/csharp/AgentSchema/Resource.cs +++ b/runtime/csharp/AgentSchema/Resource.cs @@ -89,6 +89,7 @@ private static Resource LoadKind(Dictionary data, LoadContext? { "model" => ModelResource.Load(data, context), "tool" => ToolResource.Load(data, context), + "toolbox" => ToolboxResource.Load(data, context), _ => throw new ArgumentException($"Unknown Resource discriminator value: {discriminator}"), }; } diff --git a/runtime/csharp/AgentSchema/ToolboxResource.cs b/runtime/csharp/AgentSchema/ToolboxResource.cs new file mode 100644 index 00000000..b5d49d18 --- /dev/null +++ b/runtime/csharp/AgentSchema/ToolboxResource.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace AgentSchema; +#pragma warning restore IDE0130 + +/// +/// Represents a Foundry Toolbox resource — a named collection of tools +/// that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. +/// +public class ToolboxResource : Resource +{ + /// + /// The shorthand property name for this type, if any. + /// + public new static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolboxResource() + { + } +#pragma warning restore CS8618 + + /// + /// The kind identifier for toolbox resources + /// + public override string Kind { get; set; } = "toolbox"; + + /// + /// Description of the toolbox + /// + public string? Description { get; set; } + + /// + /// The tools contained in this toolbox + /// + public IList Tools { get; set; } = []; + + + #region Load Methods + + /// + /// Load a ToolboxResource instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolboxResource instance. + public new static ToolboxResource Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolboxResource(); + + + if (data.TryGetValue("kind", out var kindValue) && kindValue is not null) + { + instance.Kind = kindValue?.ToString()!; + } + + if (data.TryGetValue("description", out var descriptionValue) && descriptionValue is not null) + { + instance.Description = descriptionValue?.ToString()!; + } + + if (data.TryGetValue("tools", out var toolsValue) && toolsValue is not null) + { + instance.Tools = LoadTools(toolsValue, context); + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + /// + /// Load a list of ToolboxTool from a dictionary or list. + /// + public static IList LoadTools(object data, LoadContext? context) + { + var result = new List(); + + if (data is Dictionary dict) + { + // Convert named dictionary to list + foreach (var kvp in dict) + { + if (kvp.Value is Dictionary itemDict) + { + // Value is an object, add name to it + itemDict["name"] = kvp.Key; + result.Add(ToolboxTool.Load(itemDict, context)); + } + else + { + // Value is a scalar, use it as the primary property + var newDict = new Dictionary + { + ["name"] = kvp.Key, + [""] = kvp.Value + }; + result.Add(ToolboxTool.Load(newDict, context)); + } + } + } + else if (data is IEnumerable list) + { + foreach (var item in list) + { + if (item is Dictionary itemDict) + { + result.Add(ToolboxTool.Load(itemDict, context)); + } + } + } + + return result; + } + + + + #endregion + + #region Save Methods + + /// + /// Save the ToolboxResource instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public override Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + // Start with parent class properties + var result = base.Save(context); + + + if (obj.Kind is not null) + { + result["kind"] = obj.Kind; + } + + if (obj.Description is not null) + { + result["description"] = obj.Description; + } + + if (obj.Tools is not null) + { + result["tools"] = SaveTools(obj.Tools, context); + } + + + return result; + } + + + /// + /// Save a list of ToolboxTool to object or array format. + /// + public static object SaveTools(IList items, SaveContext? context) + { + context ??= new SaveContext(); + + // This collection type does not have a 'name' property, only array format is supported + return items.Select(item => item.Save(context)).ToList(); + + } + + + /// + /// Convert the ToolboxResource instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public new string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolboxResource instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public new string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolboxResource instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolboxResource instance. + public new static ToolboxResource FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolboxResource instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolboxResource instance. + public new static ToolboxResource FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/csharp/AgentSchema/ToolboxTool.cs b/runtime/csharp/AgentSchema/ToolboxTool.cs new file mode 100644 index 00000000..1ee270db --- /dev/null +++ b/runtime/csharp/AgentSchema/ToolboxTool.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json; +using YamlDotNet.Serialization; + +#pragma warning disable IDE0130 +namespace AgentSchema; +#pragma warning restore IDE0130 + +/// +/// Represents a tool definition within a toolbox. +/// Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) +/// or external (mcp, openapi) with connection details. +/// +public class ToolboxTool +{ + /// + /// The shorthand property name for this type, if any. + /// + public static string? ShorthandProperty => null; + + /// + /// Initializes a new instance of . + /// +#pragma warning disable CS8618 + public ToolboxTool() + { + } +#pragma warning restore CS8618 + + /// + /// The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + /// + public string Id { get; set; } = string.Empty; + + /// + /// Optional display name for the tool + /// + public string? Name { get; set; } + + /// + /// Target endpoint URL for external tools (e.g., MCP server URL) + /// + public string? Target { get; set; } + + /// + /// Authentication type for the tool connection + /// + public string? AuthType { get; set; } + + /// + /// Additional configuration options for the tool + /// + public IDictionary? Options { get; set; } + + + #region Load Methods + + /// + /// Load a ToolboxTool instance from a dictionary. + /// + /// The dictionary containing the data. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolboxTool instance. + public static ToolboxTool Load(Dictionary data, LoadContext? context = null) + { + if (context is not null) + { + data = context.ProcessInput(data); + } + + + // Create new instance + var instance = new ToolboxTool(); + + + if (data.TryGetValue("id", out var idValue) && idValue is not null) + { + instance.Id = idValue?.ToString()!; + } + + if (data.TryGetValue("name", out var nameValue) && nameValue is not null) + { + instance.Name = nameValue?.ToString()!; + } + + if (data.TryGetValue("target", out var targetValue) && targetValue is not null) + { + instance.Target = targetValue?.ToString()!; + } + + if (data.TryGetValue("authType", out var authTypeValue) && authTypeValue is not null) + { + instance.AuthType = authTypeValue?.ToString()!; + } + + if (data.TryGetValue("options", out var optionsValue) && optionsValue is not null) + { + instance.Options = optionsValue.GetDictionary()!; + } + + if (context is not null) + { + instance = context.ProcessOutput(instance); + } + return instance; + } + + + + #endregion + + #region Save Methods + + /// + /// Save the ToolboxTool instance to a dictionary. + /// + /// Optional context with pre/post processing callbacks. + /// The dictionary representation of this instance. + public Dictionary Save(SaveContext? context = null) + { + var obj = this; + if (context is not null) + { + obj = context.ProcessObject(obj); + } + + + var result = new Dictionary(); + + + if (obj.Id is not null) + { + result["id"] = obj.Id; + } + + if (obj.Name is not null) + { + result["name"] = obj.Name; + } + + if (obj.Target is not null) + { + result["target"] = obj.Target; + } + + if (obj.AuthType is not null) + { + result["authType"] = obj.AuthType; + } + + if (obj.Options is not null) + { + result["options"] = obj.Options; + } + + + if (context is not null) + { + result = context.ProcessDict(result); + } + + return result; + } + + + /// + /// Convert the ToolboxTool instance to a YAML string. + /// + /// Optional context with pre/post processing callbacks. + /// The YAML string representation of this instance. + public string ToYaml(SaveContext? context = null) + { + context ??= new SaveContext(); + return context.ToYaml(Save(context)); + } + + /// + /// Convert the ToolboxTool instance to a JSON string. + /// + /// Optional context with pre/post processing callbacks. + /// Whether to indent the output. Defaults to true. + /// The JSON string representation of this instance. + public string ToJson(SaveContext? context = null, bool indent = true) + { + context ??= new SaveContext(); + return context.ToJson(Save(context), indent); + } + + /// + /// Load a ToolboxTool instance from a JSON string. + /// + /// The JSON string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolboxTool instance. + public static ToolboxTool FromJson(string json, LoadContext? context = null) + { + using var doc = JsonDocument.Parse(json); + Dictionary dict; + dict = JsonSerializer.Deserialize>(json, JsonUtils.Options) + ?? throw new ArgumentException("Failed to parse JSON as dictionary"); + + return Load(dict, context); + } + + /// + /// Load a ToolboxTool instance from a YAML string. + /// + /// The YAML string to parse. + /// Optional context with pre/post processing callbacks. + /// The loaded ToolboxTool instance. + public static ToolboxTool FromYaml(string yaml, LoadContext? context = null) + { + var dict = YamlUtils.Deserializer.Deserialize>(yaml) + ?? throw new ArgumentException("Failed to parse YAML as dictionary"); + + return Load(dict, context); + } + + #endregion +} diff --git a/runtime/go/agentschema/resource.go b/runtime/go/agentschema/resource.go index bd2f885c..fbcf8505 100644 --- a/runtime/go/agentschema/resource.go +++ b/runtime/go/agentschema/resource.go @@ -30,6 +30,8 @@ func LoadResource(data interface{}, ctx *LoadContext) (interface{}, error) { return LoadModelResource(data, ctx) case "tool": return LoadToolResource(data, ctx) + case "toolbox": + return LoadToolboxResource(data, ctx) } } } @@ -255,3 +257,101 @@ func ToolResourceFromYAML(yamlStr string) (ToolResource, error) { ctx := NewLoadContext() return LoadToolResource(data, ctx) } + +// ToolboxResource represents Represents a Foundry Toolbox resource — a named collection of tools +// that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. + +type ToolboxResource struct { + Kind string `json:"kind" yaml:"kind"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Tools []ToolboxTool `json:"tools" yaml:"tools"` +} + +// LoadToolboxResource creates a ToolboxResource from a map[string]interface{} +func LoadToolboxResource(data interface{}, ctx *LoadContext) (ToolboxResource, error) { + result := ToolboxResource{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["kind"]; ok && val != nil { + result.Kind = val.(string) + } + if val, ok := m["description"]; ok && val != nil { + v := val.(string) + result.Description = &v + } + if val, ok := m["tools"]; ok && val != nil { + if arr, ok := val.([]interface{}); ok { + result.Tools = make([]ToolboxTool, len(arr)) + for i, v := range arr { + if item, ok := v.(map[string]interface{}); ok { + loaded, _ := LoadToolboxTool(item, ctx) + result.Tools[i] = loaded + } + } + } + } + } + + return result, nil +} + +// Save serializes ToolboxResource to map[string]interface{} +func (obj *ToolboxResource) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["kind"] = obj.Kind + if obj.Description != nil { + result["description"] = *obj.Description + } + if obj.Tools != nil { + arr := make([]interface{}, len(obj.Tools)) + for i, item := range obj.Tools { + arr[i] = item.Save(ctx) + } + result["tools"] = arr + } + + return result +} + +// ToJSON serializes ToolboxResource to JSON string +func (obj *ToolboxResource) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolboxResource to YAML string +func (obj *ToolboxResource) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolboxResource from JSON string +func ToolboxResourceFromJSON(jsonStr string) (ToolboxResource, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolboxResource{}, err + } + ctx := NewLoadContext() + return LoadToolboxResource(data, ctx) +} + +// FromYAML creates ToolboxResource from YAML string +func ToolboxResourceFromYAML(yamlStr string) (ToolboxResource, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolboxResource{}, err + } + ctx := NewLoadContext() + return LoadToolboxResource(data, ctx) +} diff --git a/runtime/go/agentschema/toolbox_resource_test.go b/runtime/go/agentschema/toolbox_resource_test.go new file mode 100644 index 00000000..cd60c915 --- /dev/null +++ b/runtime/go/agentschema/toolbox_resource_test.go @@ -0,0 +1,231 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package agentschema_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/microsoft/agentschema-go/agentschema" +) + +// TestToolboxResourceLoadJSON tests loading ToolboxResource from JSON +func TestToolboxResourceLoadJSON(t *testing.T) { + jsonData := ` +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxResource(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxResource: %v", err) + } + if instance.Kind != "toolbox" { + t.Errorf(`Expected Kind to be "toolbox", got %v`, instance.Kind) + } + if instance.Description == nil || *instance.Description != "Shared platform tools" { + t.Errorf(`Expected Description to be "Shared platform tools", got %v`, instance.Description) + } +} + +// TestToolboxResourceLoadYAML tests loading ToolboxResource from YAML +func TestToolboxResourceLoadYAML(t *testing.T) { + yamlData := ` +kind: toolbox +description: Shared platform tools +tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxResource(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxResource: %v", err) + } + if instance.Kind != "toolbox" { + t.Errorf(`Expected Kind to be "toolbox", got %v`, instance.Kind) + } + if instance.Description == nil || *instance.Description != "Shared platform tools" { + t.Errorf(`Expected Description to be "Shared platform tools", got %v`, instance.Description) + } +} + +// TestToolboxResourceRoundtrip tests load -> save -> load produces equivalent data +func TestToolboxResourceRoundtrip(t *testing.T) { + jsonData := ` +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxResource(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolboxResource: %v", err) + } + saveCtx := agentschema.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := agentschema.LoadToolboxResource(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolboxResource: %v", err) + } + if reloaded.Kind != "toolbox" { + t.Errorf(`Expected Kind to be "toolbox", got %v`, reloaded.Kind) + } + if reloaded.Description == nil || *reloaded.Description != "Shared platform tools" { + t.Errorf(`Expected Description to be "Shared platform tools", got %v`, reloaded.Description) + } +} + +// TestToolboxResourceToJSON tests that ToJSON produces valid JSON +func TestToolboxResourceToJSON(t *testing.T) { + jsonData := ` +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxResource(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxResource: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolboxResourceToYAML tests that ToYAML produces valid YAML +func TestToolboxResourceToYAML(t *testing.T) { + jsonData := ` +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxResource(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxResource: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/go/agentschema/toolbox_tool.go b/runtime/go/agentschema/toolbox_tool.go new file mode 100644 index 00000000..c8ee263b --- /dev/null +++ b/runtime/go/agentschema/toolbox_tool.go @@ -0,0 +1,114 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package agentschema + +import ( + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// ToolboxTool represents Represents a tool definition within a toolbox. +// Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) +// or external (mcp, openapi) with connection details. + +type ToolboxTool struct { + Id string `json:"id" yaml:"id"` + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + Target *string `json:"target,omitempty" yaml:"target,omitempty"` + AuthType *string `json:"authType,omitempty" yaml:"authType,omitempty"` + Options map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` +} + +// LoadToolboxTool creates a ToolboxTool from a map[string]interface{} +func LoadToolboxTool(data interface{}, ctx *LoadContext) (ToolboxTool, error) { + result := ToolboxTool{} + + // Load from map + if m, ok := data.(map[string]interface{}); ok { + if val, ok := m["id"]; ok && val != nil { + result.Id = val.(string) + } + if val, ok := m["name"]; ok && val != nil { + v := val.(string) + result.Name = &v + } + if val, ok := m["target"]; ok && val != nil { + v := val.(string) + result.Target = &v + } + if val, ok := m["authType"]; ok && val != nil { + v := val.(string) + result.AuthType = &v + } + if val, ok := m["options"]; ok && val != nil { + if m, ok := val.(map[string]interface{}); ok { + result.Options = m + } + } + } + + return result, nil +} + +// Save serializes ToolboxTool to map[string]interface{} +func (obj *ToolboxTool) Save(ctx *SaveContext) map[string]interface{} { + result := make(map[string]interface{}) + result["id"] = obj.Id + if obj.Name != nil { + result["name"] = *obj.Name + } + if obj.Target != nil { + result["target"] = *obj.Target + } + if obj.AuthType != nil { + result["authType"] = *obj.AuthType + } + if obj.Options != nil { + result["options"] = obj.Options + } + + return result +} + +// ToJSON serializes ToolboxTool to JSON string +func (obj *ToolboxTool) ToJSON() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// ToYAML serializes ToolboxTool to YAML string +func (obj *ToolboxTool) ToYAML() (string, error) { + ctx := NewSaveContext() + data := obj.Save(ctx) + bytes, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(bytes), nil +} + +// FromJSON creates ToolboxTool from JSON string +func ToolboxToolFromJSON(jsonStr string) (ToolboxTool, error) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return ToolboxTool{}, err + } + ctx := NewLoadContext() + return LoadToolboxTool(data, ctx) +} + +// FromYAML creates ToolboxTool from YAML string +func ToolboxToolFromYAML(yamlStr string) (ToolboxTool, error) { + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &data); err != nil { + return ToolboxTool{}, err + } + ctx := NewLoadContext() + return LoadToolboxTool(data, ctx) +} diff --git a/runtime/go/agentschema/toolbox_tool_test.go b/runtime/go/agentschema/toolbox_tool_test.go new file mode 100644 index 00000000..92e552c5 --- /dev/null +++ b/runtime/go/agentschema/toolbox_tool_test.go @@ -0,0 +1,196 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +package agentschema_test + +import ( + "encoding/json" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/microsoft/agentschema-go/agentschema" +) + +// TestToolboxToolLoadJSON tests loading ToolboxTool from JSON +func TestToolboxToolLoadJSON(t *testing.T) { + jsonData := ` +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxTool(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxTool: %v", err) + } + if instance.Id != "bing_grounding" { + t.Errorf(`Expected Id to be "bing_grounding", got %v`, instance.Id) + } + if instance.Name == nil || *instance.Name != "my-search-tool" { + t.Errorf(`Expected Name to be "my-search-tool", got %v`, instance.Name) + } + if instance.Target == nil || *instance.Target != "https://api.githubcopilot.com/mcp" { + t.Errorf(`Expected Target to be "https://api.githubcopilot.com/mcp", got %v`, instance.Target) + } + if instance.AuthType == nil || *instance.AuthType != "OAuth2" { + t.Errorf(`Expected AuthType to be "OAuth2", got %v`, instance.AuthType) + } +} + +// TestToolboxToolLoadYAML tests loading ToolboxTool from YAML +func TestToolboxToolLoadYAML(t *testing.T) { + yamlData := ` +id: bing_grounding +name: my-search-tool +target: "https://api.githubcopilot.com/mcp" +authType: OAuth2 +options: + indexName: products-index + +` + var data map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlData), &data); err != nil { + t.Fatalf("Failed to parse YAML: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxTool(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxTool: %v", err) + } + if instance.Id != "bing_grounding" { + t.Errorf(`Expected Id to be "bing_grounding", got %v`, instance.Id) + } + if instance.Name == nil || *instance.Name != "my-search-tool" { + t.Errorf(`Expected Name to be "my-search-tool", got %v`, instance.Name) + } + if instance.Target == nil || *instance.Target != "https://api.githubcopilot.com/mcp" { + t.Errorf(`Expected Target to be "https://api.githubcopilot.com/mcp", got %v`, instance.Target) + } + if instance.AuthType == nil || *instance.AuthType != "OAuth2" { + t.Errorf(`Expected AuthType to be "OAuth2", got %v`, instance.AuthType) + } +} + +// TestToolboxToolRoundtrip tests load -> save -> load produces equivalent data +func TestToolboxToolRoundtrip(t *testing.T) { + jsonData := ` +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + loadCtx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxTool(data, loadCtx) + if err != nil { + t.Fatalf("Failed to load ToolboxTool: %v", err) + } + saveCtx := agentschema.NewSaveContext() + savedData := instance.Save(saveCtx) + + reloaded, err := agentschema.LoadToolboxTool(savedData, loadCtx) + if err != nil { + t.Fatalf("Failed to reload ToolboxTool: %v", err) + } + if reloaded.Id != "bing_grounding" { + t.Errorf(`Expected Id to be "bing_grounding", got %v`, reloaded.Id) + } + if reloaded.Name == nil || *reloaded.Name != "my-search-tool" { + t.Errorf(`Expected Name to be "my-search-tool", got %v`, reloaded.Name) + } + if reloaded.Target == nil || *reloaded.Target != "https://api.githubcopilot.com/mcp" { + t.Errorf(`Expected Target to be "https://api.githubcopilot.com/mcp", got %v`, reloaded.Target) + } + if reloaded.AuthType == nil || *reloaded.AuthType != "OAuth2" { + t.Errorf(`Expected AuthType to be "OAuth2", got %v`, reloaded.AuthType) + } +} + +// TestToolboxToolToJSON tests that ToJSON produces valid JSON +func TestToolboxToolToJSON(t *testing.T) { + jsonData := ` +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxTool(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxTool: %v", err) + } + jsonOutput, err := instance.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(jsonOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated JSON: %v", err) + } +} + +// TestToolboxToolToYAML tests that ToYAML produces valid YAML +func TestToolboxToolToYAML(t *testing.T) { + jsonData := ` +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +` + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonData), &data); err != nil { + t.Fatalf("Failed to parse JSON: %v", err) + } + + ctx := agentschema.NewLoadContext() + instance, err := agentschema.LoadToolboxTool(data, ctx) + if err != nil { + t.Fatalf("Failed to load ToolboxTool: %v", err) + } + yamlOutput, err := instance.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + var parsed map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlOutput), &parsed); err != nil { + t.Fatalf("Failed to parse generated YAML: %v", err) + } +} diff --git a/runtime/python/agentschema/src/agentschema/_Resource.py b/runtime/python/agentschema/src/agentschema/_Resource.py index ff65f658..41ee5c51 100644 --- a/runtime/python/agentschema/src/agentschema/_Resource.py +++ b/runtime/python/agentschema/src/agentschema/_Resource.py @@ -9,6 +9,7 @@ from typing import Any, ClassVar, Optional from ._context import LoadContext, SaveContext +from ._ToolboxTool import ToolboxTool @dataclass @@ -67,6 +68,8 @@ def load_kind(data: dict, context: Optional[LoadContext]) -> "Resource": return ModelResource.load(data, context) elif discriminator_value == "tool": return ToolResource.load(data, context) + elif discriminator_value == "toolbox": + return ToolboxResource.load(data, context) else: raise ValueError( @@ -316,3 +319,155 @@ def to_json(self, context: Optional[SaveContext] = None, indent: int = 2) -> str if context is None: context = SaveContext() return context.to_json(self.save(context), indent) + + +@dataclass +class ToolboxResource(Resource): + """Represents a Foundry Toolbox resource — a named collection of tools + that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. + + Attributes + ---------- + kind : str + The kind identifier for toolbox resources + description : Optional[str] + Description of the toolbox + tools : list[ToolboxTool] + The tools contained in this toolbox + """ + + _shorthand_property: ClassVar[Optional[str]] = None + + kind: str = field(default="toolbox") + description: Optional[str] = None + tools: list[ToolboxTool] = field(default_factory=list) + + @staticmethod + def load(data: Any, context: Optional[LoadContext] = None) -> "ToolboxResource": + """Load a ToolboxResource instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolboxResource: The loaded ToolboxResource instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolboxResource: {data}") + + # create new instance + instance = ToolboxResource() + + if data is not None and "kind" in data: + instance.kind = data["kind"] + if data is not None and "description" in data: + instance.description = data["description"] + if data is not None and "tools" in data: + instance.tools = ToolboxResource.load_tools(data["tools"], context) + if context is not None: + instance = context.process_output(instance) + return instance + + @staticmethod + def load_tools( + data: dict | list, context: Optional[LoadContext] + ) -> list[ToolboxTool]: + if isinstance(data, dict): + # convert simple named tools to list of ToolboxTool + result = [] + for k, v in data.items(): + if isinstance(v, dict): + # value is an object, spread its properties + result.append({"name": k, **v}) + else: + # value is a scalar, use it as the primary property + result.append({"name": k, "id": v}) + data = result + return [ToolboxTool.load(item, context) for item in data] + + @staticmethod + def save_tools( + items: list[ToolboxTool], context: Optional[SaveContext] + ) -> dict[str, Any] | list[dict[str, Any]]: + if context is None: + context = SaveContext() + + if context.collection_format == "array": + return [item.save(context) for item in items] + + # Object format: use name as key + result: dict[str, Any] = {} + for item in items: + item_data = item.save(context) + name = item_data.pop("name", None) + if name: + # Check if we can use shorthand (only primary property set) + if context.use_shorthand and hasattr(item, "_shorthand_property"): + shorthand_prop = item._shorthand_property + if ( + shorthand_prop + and len(item_data) == 1 + and shorthand_prop in item_data + ): + result[name] = item_data[shorthand_prop] + continue + result[name] = item_data + else: + # No name, fall back to array format for this item + if "_unnamed" not in result: + result["_unnamed"] = [] + result["_unnamed"].append(item_data) + return result + + def save(self, context: Optional[SaveContext] = None) -> dict[str, Any]: + """Save the ToolboxResource instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + # Start with parent class properties + result = super().save(context) + + if obj.kind is not None: + result["kind"] = obj.kind + if obj.description is not None: + result["description"] = obj.description + if obj.tools is not None: + result["tools"] = ToolboxResource.save_tools(obj.tools, context) + + return result + + def to_yaml(self, context: Optional[SaveContext] = None) -> str: + """Convert the ToolboxResource instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: Optional[SaveContext] = None, indent: int = 2) -> str: + """Convert the ToolboxResource instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/agentschema/src/agentschema/_ToolboxTool.py b/runtime/python/agentschema/src/agentschema/_ToolboxTool.py new file mode 100644 index 00000000..edac6e27 --- /dev/null +++ b/runtime/python/agentschema/src/agentschema/_ToolboxTool.py @@ -0,0 +1,127 @@ +########################################## +# WARNING: This is an auto-generated file. +# DO NOT EDIT THIS FILE DIRECTLY +# ANY EDITS WILL BE LOST +########################################## + +from dataclasses import dataclass, field +from typing import Any, ClassVar, Optional + +from ._context import LoadContext, SaveContext + + +@dataclass +class ToolboxTool: + """Represents a tool definition within a toolbox. + Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) + or external (mcp, openapi) with connection details. + + Attributes + ---------- + id : str + The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + name : Optional[str] + Optional display name for the tool + target : Optional[str] + Target endpoint URL for external tools (e.g., MCP server URL) + authType : Optional[str] + Authentication type for the tool connection + options : Optional[dict[str, Any]] + Additional configuration options for the tool + """ + + _shorthand_property: ClassVar[Optional[str]] = None + + id: str = field(default="") + name: Optional[str] = None + target: Optional[str] = None + authType: Optional[str] = None + options: Optional[dict[str, Any]] = None + + @staticmethod + def load(data: Any, context: Optional[LoadContext] = None) -> "ToolboxTool": + """Load a ToolboxTool instance. + Args: + data (Any): The data to load the instance from. + context (Optional[LoadContext]): Optional context with pre/post processing callbacks. + Returns: + ToolboxTool: The loaded ToolboxTool instance. + + """ + + if context is not None: + data = context.process_input(data) + + if not isinstance(data, dict): + raise ValueError(f"Invalid data for ToolboxTool: {data}") + + # create new instance + instance = ToolboxTool() + + if data is not None and "id" in data: + instance.id = data["id"] + if data is not None and "name" in data: + instance.name = data["name"] + if data is not None and "target" in data: + instance.target = data["target"] + if data is not None and "authType" in data: + instance.authType = data["authType"] + if data is not None and "options" in data: + instance.options = data["options"] + if context is not None: + instance = context.process_output(instance) + return instance + + def save(self, context: Optional[SaveContext] = None) -> dict[str, Any]: + """Save the ToolboxTool instance to a dictionary. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + dict[str, Any]: The dictionary representation of this instance. + + """ + obj = self + if context is not None: + obj = context.process_object(obj) + + result: dict[str, Any] = {} + + if obj.id is not None: + result["id"] = obj.id + if obj.name is not None: + result["name"] = obj.name + if obj.target is not None: + result["target"] = obj.target + if obj.authType is not None: + result["authType"] = obj.authType + if obj.options is not None: + result["options"] = obj.options + + if context is not None: + result = context.process_dict(result) + return result + + def to_yaml(self, context: Optional[SaveContext] = None) -> str: + """Convert the ToolboxTool instance to a YAML string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + Returns: + str: The YAML string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_yaml(self.save(context)) + + def to_json(self, context: Optional[SaveContext] = None, indent: int = 2) -> str: + """Convert the ToolboxTool instance to a JSON string. + Args: + context (Optional[SaveContext]): Optional context with pre/post processing callbacks. + indent (int): Number of spaces for indentation. Defaults to 2. + Returns: + str: The JSON string representation of this instance. + + """ + if context is None: + context = SaveContext() + return context.to_json(self.save(context), indent) diff --git a/runtime/python/agentschema/src/agentschema/__init__.py b/runtime/python/agentschema/src/agentschema/__init__.py index 652ac009..1f205964 100644 --- a/runtime/python/agentschema/src/agentschema/__init__.py +++ b/runtime/python/agentschema/src/agentschema/__init__.py @@ -72,7 +72,10 @@ from ._EnvironmentVariable import EnvironmentVariable -from ._Resource import Resource, ModelResource, ToolResource +from ._Resource import Resource, ModelResource, ToolResource, ToolboxResource + + +from ._ToolboxTool import ToolboxTool from ._AgentManifest import AgentManifest @@ -119,5 +122,7 @@ "Resource", "ModelResource", "ToolResource", + "ToolboxTool", + "ToolboxResource", "AgentManifest", ] diff --git a/runtime/python/agentschema/tests/test_toolbox_resource.py b/runtime/python/agentschema/tests/test_toolbox_resource.py new file mode 100644 index 00000000..325fe705 --- /dev/null +++ b/runtime/python/agentschema/tests/test_toolbox_resource.py @@ -0,0 +1,157 @@ +import json +import yaml + +from agentschema import ToolboxResource + + +def test_load_json_toolboxresource(): + json_data = r""" + { + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxResource.load(data) + assert instance is not None + assert instance.kind == "toolbox" + assert instance.description == "Shared platform tools" + + +def test_load_yaml_toolboxresource(): + yaml_data = r""" + kind: toolbox + description: Shared platform tools + tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolboxResource.load(data) + assert instance is not None + assert instance.kind == "toolbox" + assert instance.description == "Shared platform tools" + + +def test_roundtrip_json_toolboxresource(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolboxResource.load(original_data) + saved_data = instance.save() + reloaded = ToolboxResource.load(saved_data) + assert reloaded is not None + assert reloaded.kind == "toolbox" + assert reloaded.description == "Shared platform tools" + + +def test_to_json_toolboxresource(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxResource.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolboxresource(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxResource.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/python/agentschema/tests/test_toolbox_tool.py b/runtime/python/agentschema/tests/test_toolbox_tool.py new file mode 100644 index 00000000..61546c51 --- /dev/null +++ b/runtime/python/agentschema/tests/test_toolbox_tool.py @@ -0,0 +1,110 @@ +import json +import yaml + +from agentschema import ToolboxTool + + +def test_load_json_toolboxtool(): + json_data = r""" + { + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxTool.load(data) + assert instance is not None + assert instance.id == "bing_grounding" + assert instance.name == "my-search-tool" + assert instance.target == "https://api.githubcopilot.com/mcp" + assert instance.authType == "OAuth2" + + +def test_load_yaml_toolboxtool(): + yaml_data = r""" + id: bing_grounding + name: my-search-tool + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + options: + indexName: products-index + + """ + data = yaml.load(yaml_data, Loader=yaml.FullLoader) + instance = ToolboxTool.load(data) + assert instance is not None + assert instance.id == "bing_grounding" + assert instance.name == "my-search-tool" + assert instance.target == "https://api.githubcopilot.com/mcp" + assert instance.authType == "OAuth2" + + +def test_roundtrip_json_toolboxtool(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } + } + """ + original_data = json.loads(json_data, strict=False) + instance = ToolboxTool.load(original_data) + saved_data = instance.save() + reloaded = ToolboxTool.load(saved_data) + assert reloaded is not None + assert reloaded.id == "bing_grounding" + assert reloaded.name == "my-search-tool" + assert reloaded.target == "https://api.githubcopilot.com/mcp" + assert reloaded.authType == "OAuth2" + + +def test_to_json_toolboxtool(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxTool.load(data) + json_output = instance.to_json() + assert json_output is not None + parsed = json.loads(json_output) + assert isinstance(parsed, dict) + + +def test_to_yaml_toolboxtool(): + """Test that to_yaml produces valid YAML.""" + json_data = r""" + { + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxTool.load(data) + yaml_output = instance.to_yaml() + assert yaml_output is not None + parsed = yaml.safe_load(yaml_output) + assert isinstance(parsed, dict) diff --git a/runtime/rust/agentschema/src/lib.rs b/runtime/rust/agentschema/src/lib.rs index 53176a16..dff8e6a8 100644 --- a/runtime/rust/agentschema/src/lib.rs +++ b/runtime/rust/agentschema/src/lib.rs @@ -51,5 +51,8 @@ pub use environment_variable::*; pub mod resource; pub use resource::*; +pub mod toolbox_tool; +pub use toolbox_tool::*; + pub mod agent_manifest; pub use agent_manifest::*; diff --git a/runtime/rust/agentschema/src/resource.rs b/runtime/rust/agentschema/src/resource.rs index 12bccc86..2c267087 100644 --- a/runtime/rust/agentschema/src/resource.rs +++ b/runtime/rust/agentschema/src/resource.rs @@ -1,5 +1,7 @@ // Code generated by AgentSchema emitter; DO NOT EDIT. +use crate::toolbox_tool::ToolboxTool; + /// Represents a resource required by the agent Resources can include databases, APIs, or other external systems that the agent needs to interact with to perform its tasks #[derive(Debug, Clone, Default)] pub struct Resource { @@ -223,3 +225,112 @@ impl ToolResource { self.options.as_object() } } + +/// Represents a Foundry Toolbox resource — a named collection of tools that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. +#[derive(Debug, Clone, Default)] +pub struct ToolboxResource { + /// The kind identifier for toolbox resources + pub kind: String, + /// Description of the toolbox + pub description: Option, + /// The tools contained in this toolbox + pub tools: serde_json::Value, +} + +impl ToolboxResource { + /// Create a new ToolboxResource with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load ToolboxResource from a JSON string. + pub fn from_json(json: &str) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value)) + } + + /// Load ToolboxResource from a YAML string. + pub fn from_yaml(yaml: &str) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value)) + } + + /// Load ToolboxResource from a `serde_json::Value`. + pub fn load_from_value(value: &serde_json::Value) -> Self { + Self { + kind: value + .get("kind") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + description: value + .get("description") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + tools: value + .get("tools") + .cloned() + .unwrap_or(serde_json::Value::Null), + } + } + + /// Serialize ToolboxResource to a `serde_json::Value`. + pub fn to_value(&self) -> serde_json::Value { + let mut result = serde_json::Map::new(); + if !self.kind.is_empty() { + result.insert( + "kind".to_string(), + serde_json::Value::String(self.kind.clone()), + ); + } + if let Some(ref val) = self.description { + result.insert( + "description".to_string(), + serde_json::Value::String(val.clone()), + ); + } + if !self.tools.is_null() { + result.insert("tools".to_string(), self.tools.clone()); + } + serde_json::Value::Object(result) + } + + /// Serialize ToolboxResource to a JSON string. + pub fn to_json(&self) -> Result { + serde_json::to_string_pretty(&self.to_value()) + } + + /// Serialize ToolboxResource to a YAML string. + pub fn to_yaml(&self) -> Result { + serde_yaml::to_string(&self.to_value()) + } + /// Returns typed `Vec` by parsing the stored JSON value. + /// Handles both array format `[{...}]` and dict format `{"name": {...}}`. + /// Returns `None` if the field is null or cannot be parsed. + pub fn as_tools(&self) -> Option> { + match &self.tools { + serde_json::Value::Array(arr) => { + Some(arr.iter().map(ToolboxTool::load_from_value).collect()) + } + serde_json::Value::Object(obj) => { + let result: Vec = obj + .iter() + .map(|(name, value)| { + let mut v = if value.is_object() { + value.clone() + } else { + serde_json::json!({ "value": value }) + }; + if let serde_json::Value::Object(ref mut m) = v { + m.entry("name".to_string()) + .or_insert_with(|| serde_json::Value::String(name.clone())); + } + ToolboxTool::load_from_value(&v) + }) + .collect(); + Some(result) + } + _ => None, + } + } +} diff --git a/runtime/rust/agentschema/src/toolbox_tool.rs b/runtime/rust/agentschema/src/toolbox_tool.rs new file mode 100644 index 00000000..0be29e7b --- /dev/null +++ b/runtime/rust/agentschema/src/toolbox_tool.rs @@ -0,0 +1,101 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +/// Represents a tool definition within a toolbox. Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) or external (mcp, openapi) with connection details. +#[derive(Debug, Clone, Default)] +pub struct ToolboxTool { + /// The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + pub id: String, + /// Optional display name for the tool + pub name: Option, + /// Target endpoint URL for external tools (e.g., MCP server URL) + pub target: Option, + /// Authentication type for the tool connection + pub auth_type: Option, + /// Additional configuration options for the tool + pub options: serde_json::Value, +} + +impl ToolboxTool { + /// Create a new ToolboxTool with default values. + pub fn new() -> Self { + Self::default() + } + + /// Load ToolboxTool from a JSON string. + pub fn from_json(json: &str) -> Result { + let value: serde_json::Value = serde_json::from_str(json)?; + Ok(Self::load_from_value(&value)) + } + + /// Load ToolboxTool from a YAML string. + pub fn from_yaml(yaml: &str) -> Result { + let value: serde_json::Value = serde_yaml::from_str(yaml)?; + Ok(Self::load_from_value(&value)) + } + + /// Load ToolboxTool from a `serde_json::Value`. + pub fn load_from_value(value: &serde_json::Value) -> Self { + Self { + id: value + .get("id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + name: value + .get("name") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + target: value + .get("target") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + auth_type: value + .get("authType") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + options: value + .get("options") + .cloned() + .unwrap_or(serde_json::Value::Null), + } + } + + /// Serialize ToolboxTool to a `serde_json::Value`. + pub fn to_value(&self) -> serde_json::Value { + let mut result = serde_json::Map::new(); + if !self.id.is_empty() { + result.insert("id".to_string(), serde_json::Value::String(self.id.clone())); + } + if let Some(ref val) = self.name { + result.insert("name".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(ref val) = self.target { + result.insert("target".to_string(), serde_json::Value::String(val.clone())); + } + if let Some(ref val) = self.auth_type { + result.insert( + "authType".to_string(), + serde_json::Value::String(val.clone()), + ); + } + if !self.options.is_null() { + result.insert("options".to_string(), self.options.clone()); + } + serde_json::Value::Object(result) + } + + /// Serialize ToolboxTool to a JSON string. + pub fn to_json(&self) -> Result { + serde_json::to_string_pretty(&self.to_value()) + } + + /// Serialize ToolboxTool to a YAML string. + pub fn to_yaml(&self) -> Result { + serde_yaml::to_string(&self.to_value()) + } + /// Returns typed reference to the map if the field is an object. + /// Returns `None` if the field is null or not an object. + pub fn as_options_dict(&self) -> Option<&serde_json::Map> { + self.options.as_object() + } +} diff --git a/runtime/rust/agentschema/tests/toolbox_resource_test.rs b/runtime/rust/agentschema/tests/toolbox_resource_test.rs new file mode 100644 index 00000000..d07c3346 --- /dev/null +++ b/runtime/rust/agentschema/tests/toolbox_resource_test.rs @@ -0,0 +1,112 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +use agentschema::ToolboxResource; + +#[test] +fn test_toolbox_resource_load_json() { + let json = r####" +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +"####; + let result = ToolboxResource::from_json(json); + assert!( + result.is_ok(), + "Failed to load from JSON: {:?}", + result.err() + ); + let instance = result.unwrap(); + assert_eq!(instance.kind, "toolbox"); + assert!( + instance.description.is_some(), + "Expected description to be Some" + ); + assert_eq!( + instance.description.as_ref().unwrap(), + &"Shared platform tools" + ); +} + +#[test] +fn test_toolbox_resource_load_yaml() { + let yaml = r####" +kind: toolbox +description: Shared platform tools +tools: + - id: bing_grounding + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + name: github-copilot + target: "https://api.githubcopilot.com/mcp" + authType: OAuth2 + +"####; + let result = ToolboxResource::from_yaml(yaml); + assert!( + result.is_ok(), + "Failed to load from YAML: {:?}", + result.err() + ); + let instance = result.unwrap(); + assert_eq!(instance.kind, "toolbox"); + assert!( + instance.description.is_some(), + "Expected description to be Some" + ); +} + +#[test] +fn test_toolbox_resource_roundtrip() { + let json = r####" +{ + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "bing_grounding" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "name": "github-copilot", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2" + } + ] +} +"####; + let result = ToolboxResource::from_json(json); + assert!(result.is_ok(), "Failed to load: {:?}", result.err()); + let instance = result.unwrap(); + let json_output = instance.to_json(); + assert!( + json_output.is_ok(), + "Failed to serialize to JSON: {:?}", + json_output.err() + ); +} diff --git a/runtime/rust/agentschema/tests/toolbox_tool_test.rs b/runtime/rust/agentschema/tests/toolbox_tool_test.rs new file mode 100644 index 00000000..fed92e77 --- /dev/null +++ b/runtime/rust/agentschema/tests/toolbox_tool_test.rs @@ -0,0 +1,89 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +use agentschema::ToolboxTool; + +#[test] +fn test_toolbox_tool_load_json() { + let json = r####" +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +"####; + let result = ToolboxTool::from_json(json); + assert!( + result.is_ok(), + "Failed to load from JSON: {:?}", + result.err() + ); + let instance = result.unwrap(); + assert_eq!(instance.id, "bing_grounding"); + assert!(instance.name.is_some(), "Expected name to be Some"); + assert_eq!(instance.name.as_ref().unwrap(), &"my-search-tool"); + assert!(instance.target.is_some(), "Expected target to be Some"); + assert_eq!( + instance.target.as_ref().unwrap(), + &"https://api.githubcopilot.com/mcp" + ); + assert!( + instance.auth_type.is_some(), + "Expected auth_type to be Some" + ); + assert_eq!(instance.auth_type.as_ref().unwrap(), &"OAuth2"); +} + +#[test] +fn test_toolbox_tool_load_yaml() { + let yaml = r####" +id: bing_grounding +name: my-search-tool +target: "https://api.githubcopilot.com/mcp" +authType: OAuth2 +options: + indexName: products-index + +"####; + let result = ToolboxTool::from_yaml(yaml); + assert!( + result.is_ok(), + "Failed to load from YAML: {:?}", + result.err() + ); + let instance = result.unwrap(); + assert_eq!(instance.id, "bing_grounding"); + assert!(instance.name.is_some(), "Expected name to be Some"); + assert!(instance.target.is_some(), "Expected target to be Some"); + assert!( + instance.auth_type.is_some(), + "Expected auth_type to be Some" + ); +} + +#[test] +fn test_toolbox_tool_roundtrip() { + let json = r####" +{ + "id": "bing_grounding", + "name": "my-search-tool", + "target": "https://api.githubcopilot.com/mcp", + "authType": "OAuth2", + "options": { + "indexName": "products-index" + } +} +"####; + let result = ToolboxTool::from_json(json); + assert!(result.is_ok(), "Failed to load: {:?}", result.err()); + let instance = result.unwrap(); + let json_output = instance.to_json(); + assert!( + json_output.is_ok(), + "Failed to serialize to JSON: {:?}", + json_output.err() + ); +} diff --git a/runtime/typescript/agentschema/src/index.ts b/runtime/typescript/agentschema/src/index.ts index 6af680a9..65fb77b5 100644 --- a/runtime/typescript/agentschema/src/index.ts +++ b/runtime/typescript/agentschema/src/index.ts @@ -55,6 +55,8 @@ export { ContainerResources } from "./container-resources"; export { EnvironmentVariable } from "./environment-variable"; -export { Resource, ModelResource, ToolResource } from "./resource"; +export { Resource, ModelResource, ToolResource, ToolboxResource } from "./resource"; + +export { ToolboxTool } from "./toolbox-tool"; export { AgentManifest } from "./agent-manifest"; diff --git a/runtime/typescript/agentschema/src/resource.ts b/runtime/typescript/agentschema/src/resource.ts index f7cc438c..5887f10c 100644 --- a/runtime/typescript/agentschema/src/resource.ts +++ b/runtime/typescript/agentschema/src/resource.ts @@ -2,6 +2,7 @@ // WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY. import { LoadContext, SaveContext } from "./context"; +import { ToolboxTool } from "./toolbox-tool"; /** * Represents a resource required by the agent @@ -79,6 +80,8 @@ export abstract class Resource { return ModelResource.load(data, context); case "tool": return ToolResource.load(data, context); + case "toolbox": + return ToolboxResource.load(data, context); default: throw new Error(`Unknown Resource discriminator value: ${discriminator}`); } @@ -451,3 +454,209 @@ export class ToolResource extends Resource { //#endregion } + +/** + * Represents a Foundry Toolbox resource — a named collection of tools + * that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. + * + */ +export class ToolboxResource extends Resource { + /** + * The shorthand property name for this type, if any. + */ + static readonly shorthandProperty: string | undefined = undefined; + + /** + * The kind identifier for toolbox resources + */ + kind: string = "toolbox"; + + /** + * Description of the toolbox + */ + description?: string | undefined; + + /** + * The tools contained in this toolbox + */ + tools: ToolboxTool[] = []; + + /** + * Initializes a new instance of ToolboxResource. + */ + constructor(init?: Partial) { + super(init); + + this.kind = init?.kind ?? "toolbox"; + + if (init?.description !== undefined) { + this.description = init.description; + } + + this.tools = init?.tools ?? []; + } + + //#region Load Methods + + /** + * Load a ToolboxResource instance from a dictionary. + * @param data - The dictionary containing the data. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded ToolboxResource instance. + */ + static load(data: Record, context?: LoadContext): ToolboxResource { + if (context) { + data = context.processInput(data); + } + + // Create new instance + const instance = new ToolboxResource(); + + if (data["kind"] !== undefined && data["kind"] !== null) { + instance.kind = String(data["kind"]); + } + + if (data["description"] !== undefined && data["description"] !== null) { + instance.description = String(data["description"]); + } + + if (data["tools"] !== undefined && data["tools"] !== null) { + instance.tools = ToolboxResource.loadTools(data["tools"], context); + } + + if (context) { + return context.processOutput(instance) as ToolboxResource; + } + return instance; + } + + /** + * Load a collection of ToolboxTool from a dictionary or array. + * @param data - The data to load from. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded array of ToolboxTool. + */ + static loadTools(data: unknown, context?: LoadContext): ToolboxTool[] { + const result: ToolboxTool[] = []; + + if (data && typeof data === "object" && !Array.isArray(data)) { + // Convert named dictionary to array + for (const [key, value] of Object.entries(data as Record)) { + if (value && typeof value === "object" && !Array.isArray(value)) { + // Value is an object, add name to it + (value as Record)["name"] = key; + result.push(ToolboxTool.load(value as Record, context)); + } else { + // Value is a scalar, use it as the primary property + const newObj: Record = { + name: key, + id: value, + }; + result.push(ToolboxTool.load(newObj, context)); + } + } + } else if (Array.isArray(data)) { + for (const item of data) { + if (item && typeof item === "object") { + result.push(ToolboxTool.load(item as Record, context)); + } + } + } + + return result; + } + + //#endregion + + //#region Save Methods + + /** + * Save the ToolboxResource instance to a dictionary. + * @param context - Optional context with pre/post processing callbacks. + * @returns The dictionary representation of this instance. + */ + save(context?: SaveContext): Record { + const obj = context ? (context.processObject(this) as ToolboxResource) : this; + + // Start with parent class properties + const result = super.save(context); + + if (obj.kind !== undefined && obj.kind !== null) { + result["kind"] = obj.kind; + } + + if (obj.description !== undefined && obj.description !== null) { + result["description"] = obj.description; + } + + if (obj.tools !== undefined && obj.tools !== null) { + result["tools"] = ToolboxResource.saveTools(obj.tools, context); + } + + return result; + } + + /** + * Save a collection of ToolboxTool to object or array format. + * @param items - The items to save. + * @param context - Optional context with pre/post processing callbacks. + * @returns The saved collection in object or array format. + */ + static saveTools( + items: ToolboxTool[], + context?: SaveContext + ): Record | Record[] { + context = context ?? new SaveContext(); + + // This type doesn't have a 'name' property, so always use array format + return items.map((item) => item.save(context)); + } + + /** + * Convert the ToolboxResource instance to a YAML string. + * @param context - Optional context with pre/post processing callbacks. + * @returns The YAML string representation of this instance. + */ + toYaml(context?: SaveContext): string { + context = context ?? new SaveContext(); + return context.toYaml(this.save(context)); + } + + /** + * Convert the ToolboxResource instance to a JSON string. + * @param context - Optional context with pre/post processing callbacks. + * @param indent - Number of spaces for indentation. Defaults to 2. + * @returns The JSON string representation of this instance. + */ + toJson(context?: SaveContext, indent: number = 2): string { + context = context ?? new SaveContext(); + return context.toJson(this.save(context), indent); + } + + /** + * Load a ToolboxResource instance from a JSON string. + * @param json - The JSON string to parse. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded ToolboxResource instance. + */ + static fromJson(json: string, context?: LoadContext): ToolboxResource { + const data = JSON.parse(json); + + return ToolboxResource.load(data as Record, context); + } + + /** + * Load a ToolboxResource instance from a YAML string. + * @param yaml - The YAML string to parse. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded ToolboxResource instance. + */ + static fromYaml(yaml: string, context?: LoadContext): ToolboxResource { + const { parse } = require("yaml"); + const data = parse(yaml); + + return ToolboxResource.load(data as Record, context); + } + + //#endregion +} diff --git a/runtime/typescript/agentschema/src/toolbox-tool.ts b/runtime/typescript/agentschema/src/toolbox-tool.ts new file mode 100644 index 00000000..0d8a41e2 --- /dev/null +++ b/runtime/typescript/agentschema/src/toolbox-tool.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft. All rights reserved. +// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY. + +import { LoadContext, SaveContext } from "./context"; + +/** + * Represents a tool definition within a toolbox. + * Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) + * or external (mcp, openapi) with connection details. + * + */ +export class ToolboxTool { + /** + * The shorthand property name for this type, if any. + */ + static readonly shorthandProperty: string | undefined = undefined; + + /** + * The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + */ + id: string = ""; + + /** + * Optional display name for the tool + */ + name?: string | undefined; + + /** + * Target endpoint URL for external tools (e.g., MCP server URL) + */ + target?: string | undefined; + + /** + * Authentication type for the tool connection + */ + authType?: string | undefined; + + /** + * Additional configuration options for the tool + */ + options?: Record | undefined = {}; + + /** + * Initializes a new instance of ToolboxTool. + */ + constructor(init?: Partial) { + this.id = init?.id ?? ""; + + if (init?.name !== undefined) { + this.name = init.name; + } + + if (init?.target !== undefined) { + this.target = init.target; + } + + if (init?.authType !== undefined) { + this.authType = init.authType; + } + + if (init?.options !== undefined) { + this.options = init.options; + } + } + + //#region Load Methods + + /** + * Load a ToolboxTool instance from a dictionary. + * @param data - The dictionary containing the data. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded ToolboxTool instance. + */ + static load(data: Record, context?: LoadContext): ToolboxTool { + if (context) { + data = context.processInput(data); + } + + // Create new instance + const instance = new ToolboxTool(); + + if (data["id"] !== undefined && data["id"] !== null) { + instance.id = String(data["id"]); + } + + if (data["name"] !== undefined && data["name"] !== null) { + instance.name = String(data["name"]); + } + + if (data["target"] !== undefined && data["target"] !== null) { + instance.target = String(data["target"]); + } + + if (data["authType"] !== undefined && data["authType"] !== null) { + instance.authType = String(data["authType"]); + } + + if (data["options"] !== undefined && data["options"] !== null) { + instance.options = data["options"] as Record; + } + + if (context) { + return context.processOutput(instance) as ToolboxTool; + } + return instance; + } + + //#endregion + + //#region Save Methods + + /** + * Save the ToolboxTool instance to a dictionary. + * @param context - Optional context with pre/post processing callbacks. + * @returns The dictionary representation of this instance. + */ + save(context?: SaveContext): Record { + const obj = context ? (context.processObject(this) as ToolboxTool) : this; + + const result: Record = {}; + + if (obj.id !== undefined && obj.id !== null) { + result["id"] = obj.id; + } + + if (obj.name !== undefined && obj.name !== null) { + result["name"] = obj.name; + } + + if (obj.target !== undefined && obj.target !== null) { + result["target"] = obj.target; + } + + if (obj.authType !== undefined && obj.authType !== null) { + result["authType"] = obj.authType; + } + + if (obj.options !== undefined && obj.options !== null) { + result["options"] = obj.options; + } + + if (context) { + return context.processDict(result); + } + + return result; + } + + /** + * Convert the ToolboxTool instance to a YAML string. + * @param context - Optional context with pre/post processing callbacks. + * @returns The YAML string representation of this instance. + */ + toYaml(context?: SaveContext): string { + context = context ?? new SaveContext(); + return context.toYaml(this.save(context)); + } + + /** + * Convert the ToolboxTool instance to a JSON string. + * @param context - Optional context with pre/post processing callbacks. + * @param indent - Number of spaces for indentation. Defaults to 2. + * @returns The JSON string representation of this instance. + */ + toJson(context?: SaveContext, indent: number = 2): string { + context = context ?? new SaveContext(); + return context.toJson(this.save(context), indent); + } + + /** + * Load a ToolboxTool instance from a JSON string. + * @param json - The JSON string to parse. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded ToolboxTool instance. + */ + static fromJson(json: string, context?: LoadContext): ToolboxTool { + const data = JSON.parse(json); + + return ToolboxTool.load(data as Record, context); + } + + /** + * Load a ToolboxTool instance from a YAML string. + * @param yaml - The YAML string to parse. + * @param context - Optional context with pre/post processing callbacks. + * @returns The loaded ToolboxTool instance. + */ + static fromYaml(yaml: string, context?: LoadContext): ToolboxTool { + const { parse } = require("yaml"); + const data = parse(yaml); + + return ToolboxTool.load(data as Record, context); + } + + //#endregion +} diff --git a/runtime/typescript/agentschema/tests/toolbox-resource.test.ts b/runtime/typescript/agentschema/tests/toolbox-resource.test.ts new file mode 100644 index 00000000..fac37f6c --- /dev/null +++ b/runtime/typescript/agentschema/tests/toolbox-resource.test.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY. + +import { ToolboxResource } from "../src/index"; + +describe("ToolboxResource", () => { + describe("construction", () => { + it("should create a new instance with defaults", () => { + const instance = new ToolboxResource(); + expect(instance).toBeDefined(); + }); + + it("should create a new instance with partial initialization", () => { + const instance = new ToolboxResource({}); + expect(instance).toBeDefined(); + }); + }); + + describe("JSON serialization", () => { + it("should load from JSON - example 1", () => { + const json = `{\n "kind": "toolbox",\n "description": "Shared platform tools",\n "tools": [\n {\n "id": "bing_grounding"\n },\n {\n "id": "azure_ai_search",\n "options": {\n "indexName": "products-index"\n }\n },\n {\n "id": "mcp",\n "name": "github-copilot",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2"\n }\n ]\n}`; + const instance = ToolboxResource.fromJson(json); + expect(instance).toBeDefined(); + + expect(instance.kind).toEqual("toolbox"); + + expect(instance.description).toEqual("Shared platform tools"); + }); + + it("should round-trip JSON - example 1", () => { + const json = `{\n "kind": "toolbox",\n "description": "Shared platform tools",\n "tools": [\n {\n "id": "bing_grounding"\n },\n {\n "id": "azure_ai_search",\n "options": {\n "indexName": "products-index"\n }\n },\n {\n "id": "mcp",\n "name": "github-copilot",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2"\n }\n ]\n}`; + const instance = ToolboxResource.fromJson(json); + const output = instance.toJson(); + const reloaded = ToolboxResource.fromJson(output); + + expect(reloaded.kind).toEqual(instance.kind); + + expect(reloaded.description).toEqual(instance.description); + }); + }); + + describe("YAML serialization", () => { + it("should load from YAML - example 1", () => { + const yaml = `kind: toolbox\ndescription: Shared platform tools\ntools:\n - id: bing_grounding\n - id: azure_ai_search\n options:\n indexName: products-index\n - id: mcp\n name: github-copilot\n target: "https://api.githubcopilot.com/mcp"\n authType: OAuth2\n`; + const instance = ToolboxResource.fromYaml(yaml); + expect(instance).toBeDefined(); + + expect(instance.kind).toEqual("toolbox"); + + expect(instance.description).toEqual("Shared platform tools"); + }); + + it("should round-trip YAML - example 1", () => { + const yaml = `kind: toolbox\ndescription: Shared platform tools\ntools:\n - id: bing_grounding\n - id: azure_ai_search\n options:\n indexName: products-index\n - id: mcp\n name: github-copilot\n target: "https://api.githubcopilot.com/mcp"\n authType: OAuth2\n`; + const instance = ToolboxResource.fromYaml(yaml); + const output = instance.toYaml(); + const reloaded = ToolboxResource.fromYaml(output); + + expect(reloaded.kind).toEqual(instance.kind); + + expect(reloaded.description).toEqual(instance.description); + }); + }); + + describe("load and save", () => { + it("should load from dictionary", () => { + const data: Record = {}; + const instance = ToolboxResource.load(data); + expect(instance).toBeDefined(); + }); + + it("should save to dictionary", () => { + const instance = new ToolboxResource(); + const data = instance.save(); + expect(data).toBeDefined(); + expect(typeof data).toBe("object"); + }); + }); +}); diff --git a/runtime/typescript/agentschema/tests/toolbox-tool.test.ts b/runtime/typescript/agentschema/tests/toolbox-tool.test.ts new file mode 100644 index 00000000..ba55bb63 --- /dev/null +++ b/runtime/typescript/agentschema/tests/toolbox-tool.test.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. +// WARNING: This is an auto-generated file. DO NOT EDIT THIS FILE DIRECTLY. + +import { ToolboxTool } from "../src/index"; + +describe("ToolboxTool", () => { + describe("construction", () => { + it("should create a new instance with defaults", () => { + const instance = new ToolboxTool(); + expect(instance).toBeDefined(); + }); + + it("should create a new instance with partial initialization", () => { + const instance = new ToolboxTool({}); + expect(instance).toBeDefined(); + }); + }); + + describe("JSON serialization", () => { + it("should load from JSON - example 1", () => { + const json = `{\n "id": "bing_grounding",\n "name": "my-search-tool",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2",\n "options": {\n "indexName": "products-index"\n }\n}`; + const instance = ToolboxTool.fromJson(json); + expect(instance).toBeDefined(); + + expect(instance.id).toEqual("bing_grounding"); + + expect(instance.name).toEqual("my-search-tool"); + + expect(instance.target).toEqual("https://api.githubcopilot.com/mcp"); + + expect(instance.authType).toEqual("OAuth2"); + }); + + it("should round-trip JSON - example 1", () => { + const json = `{\n "id": "bing_grounding",\n "name": "my-search-tool",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2",\n "options": {\n "indexName": "products-index"\n }\n}`; + const instance = ToolboxTool.fromJson(json); + const output = instance.toJson(); + const reloaded = ToolboxTool.fromJson(output); + + expect(reloaded.id).toEqual(instance.id); + + expect(reloaded.name).toEqual(instance.name); + + expect(reloaded.target).toEqual(instance.target); + + expect(reloaded.authType).toEqual(instance.authType); + }); + }); + + describe("YAML serialization", () => { + it("should load from YAML - example 1", () => { + const yaml = `id: bing_grounding\nname: my-search-tool\ntarget: "https://api.githubcopilot.com/mcp"\nauthType: OAuth2\noptions:\n indexName: products-index\n`; + const instance = ToolboxTool.fromYaml(yaml); + expect(instance).toBeDefined(); + + expect(instance.id).toEqual("bing_grounding"); + + expect(instance.name).toEqual("my-search-tool"); + + expect(instance.target).toEqual("https://api.githubcopilot.com/mcp"); + + expect(instance.authType).toEqual("OAuth2"); + }); + + it("should round-trip YAML - example 1", () => { + const yaml = `id: bing_grounding\nname: my-search-tool\ntarget: "https://api.githubcopilot.com/mcp"\nauthType: OAuth2\noptions:\n indexName: products-index\n`; + const instance = ToolboxTool.fromYaml(yaml); + const output = instance.toYaml(); + const reloaded = ToolboxTool.fromYaml(output); + + expect(reloaded.id).toEqual(instance.id); + + expect(reloaded.name).toEqual(instance.name); + + expect(reloaded.target).toEqual(instance.target); + + expect(reloaded.authType).toEqual(instance.authType); + }); + }); + + describe("load and save", () => { + it("should load from dictionary", () => { + const data: Record = {}; + const instance = ToolboxTool.load(data); + expect(instance).toBeDefined(); + }); + + it("should save to dictionary", () => { + const instance = new ToolboxTool(); + const data = instance.save(); + expect(data).toBeDefined(); + expect(typeof data).toBe("object"); + }); + }); +}); diff --git a/schemas/v1.0/ToolboxResource.yaml b/schemas/v1.0/ToolboxResource.yaml new file mode 100644 index 00000000..8608628a --- /dev/null +++ b/schemas/v1.0/ToolboxResource.yaml @@ -0,0 +1,24 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: ToolboxResource.yaml +type: object +properties: + kind: + type: string + const: toolbox + description: The kind identifier for toolbox resources + description: + type: string + description: Description of the toolbox + tools: + type: array + items: + $ref: ToolboxTool.yaml + description: The tools contained in this toolbox +required: + - kind + - tools +allOf: + - $ref: Resource.yaml +description: |- + Represents a Foundry Toolbox resource — a named collection of tools + that is provisioned as a Foundry Toolbox and exposed via MCP endpoint. diff --git a/schemas/v1.0/ToolboxTool.yaml b/schemas/v1.0/ToolboxTool.yaml new file mode 100644 index 00000000..f2e2673e --- /dev/null +++ b/schemas/v1.0/ToolboxTool.yaml @@ -0,0 +1,50 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: ToolboxTool.yaml +type: object +properties: + id: + anyOf: + - type: string + const: bing_grounding + - type: string + const: azure_ai_search + - type: string + const: code_interpreter + - type: string + const: file_search + - type: string + const: mcp + - type: string + const: openapi + - type: string + description: The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + name: + type: string + description: Optional display name for the tool + target: + type: string + description: Target endpoint URL for external tools (e.g., MCP server URL) + authType: + anyOf: + - type: string + const: CustomKeys + - type: string + const: OAuth2 + - type: string + const: UserEntraToken + - type: string + const: ProjectManagedIdentity + - type: string + const: AgenticIdentityToken + - type: string + description: Authentication type for the tool connection + options: + $ref: RecordUnknown.yaml + default: {} + description: Additional configuration options for the tool +required: + - id +description: |- + Represents a tool definition within a toolbox. + Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) + or external (mcp, openapi) with connection details. From c84073692f20bf7f2f8e7b5216a5a833b0a409d1 Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 3 Apr 2026 14:03:01 -0400 Subject: [PATCH 2/2] Refactor ToolboxTool and ToolboxResource to update tool identifiers and add descriptions - Changed tool ID from "bing_grounding" to "web_search" across multiple test files and schemas. - Introduced a new tool "a2a_preview" with associated properties in JSON and YAML representations. - Updated ToolboxTool struct to include a description field for better clarity on tool capabilities. - Modified JSON and YAML loading/saving functions to handle the new description field. - Created new schema files for ToolboxAuthTypes and ToolboxToolTypes to standardize tool type identifiers and authentication types. - Ensured all tests reflect the updated tool identifiers and descriptions for consistency. --- agentschema-emitter/lib/model/manifest.tsp | 87 ++++++++++++++----- docs/src/content/docs/reference/README.md | 1 + .../content/docs/reference/ToolboxResource.md | 7 +- .../src/content/docs/reference/ToolboxTool.md | 13 +-- examples/toolbox/toolbox_manifest.yaml | 6 +- .../ToolboxResourceConversionTests.cs | 42 +++++++-- .../ToolboxToolConversionTests.cs | 30 ++++--- runtime/csharp/AgentSchema/ToolboxTool.cs | 23 ++++- .../go/agentschema/toolbox_resource_test.go | 38 ++++++-- runtime/go/agentschema/toolbox_tool.go | 22 +++-- runtime/go/agentschema/toolbox_tool_test.go | 36 +++++--- .../src/agentschema/_ToolboxTool.py | 15 +++- .../tests/test_toolbox_resource.py | 38 ++++++-- .../agentschema/tests/test_toolbox_tool.py | 24 +++-- runtime/rust/agentschema/src/toolbox_tool.rs | 18 +++- .../tests/toolbox_resource_test.rs | 22 ++++- .../agentschema/tests/toolbox_tool_test.rs | 25 ++++-- .../agentschema/src/toolbox-tool.ts | 25 +++++- .../tests/toolbox-resource.test.ts | 8 +- .../agentschema/tests/toolbox-tool.test.ts | 20 +++-- schemas/v1.0/ToolboxAuthTypes.yaml | 20 +++++ schemas/v1.0/ToolboxTool.yaml | 39 ++------- schemas/v1.0/ToolboxToolTypes.yaml | 26 ++++++ 23 files changed, 439 insertions(+), 146 deletions(-) create mode 100644 schemas/v1.0/ToolboxAuthTypes.yaml create mode 100644 schemas/v1.0/ToolboxToolTypes.yaml diff --git a/agentschema-emitter/lib/model/manifest.tsp b/agentschema-emitter/lib/model/manifest.tsp index 082c9452..e6ab549e 100644 --- a/agentschema-emitter/lib/model/manifest.tsp +++ b/agentschema-emitter/lib/model/manifest.tsp @@ -140,44 +140,77 @@ alias Resources = Record | Named< #{ name: "my-resource" } >[]; -alias ToolboxToolId = - | "bing_grounding" - | "azure_ai_search" - | "code_interpreter" - | "file_search" - | "mcp" - | "openapi" - | string; - -alias ToolboxAuthType = - | "CustomKeys" - | "OAuth2" - | "UserEntraToken" - | "ProjectManagedIdentity" - | "AgenticIdentityToken" - | string; +@doc("Known toolbox tool type identifiers") +union ToolboxToolTypes { + @doc("Model Context Protocol server") + mcp: "mcp", + + @doc("Web search via Bing grounding") + web_search: "web_search", + + @doc("Azure AI Search index") + azure_ai_search: "azure_ai_search", + + @doc("OpenAPI specification endpoint") + openapi: "openapi", + + @doc("Agent-to-agent delegation (preview)") + a2a_preview: "a2a_preview", + + @doc("Sandboxed Python code execution") + code_interpreter: "code_interpreter", + + @doc("Vector store file search") + file_search: "file_search", + + string, +} + +@doc("Authentication types for toolbox tool connections") +union ToolboxAuthTypes { + @doc("Custom API key or PAT-based authentication") + CustomKeys: "CustomKeys", + + @doc("OAuth 2.0 with identity passthrough") + OAuth2: "OAuth2", + + @doc("Microsoft Entra ID user token") + UserEntraToken: "UserEntraToken", + + @doc("Foundry project managed identity") + ProjectManagedIdentity: "ProjectManagedIdentity", + + @doc("Agentic identity token (preview)") + AgenticIdentityToken: "AgenticIdentityToken", + + string, +} /** * Represents a tool definition within a toolbox. - * Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) - * or external (mcp, openapi) with connection details. + * Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) + * or external (mcp, openapi, a2a_preview) with connection details. */ model ToolboxTool { - @doc("The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp')") - @sample(#{ id: "bing_grounding" }) - id: ToolboxToolId; + @doc("The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview')") + @sample(#{ id: "web_search" }) + id: ToolboxToolTypes; @doc("Optional display name for the tool") @sample(#{ name: "my-search-tool" }) name?: string; - @doc("Target endpoint URL for external tools (e.g., MCP server URL)") + @doc("Human-readable description of the tool's capabilities") + @sample(#{ description: "Searches the web for up-to-date information" }) + description?: string; + + @doc("Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL)") @sample(#{ target: "https://api.githubcopilot.com/mcp" }) target?: string; @doc("Authentication type for the tool connection") @sample(#{ authType: "OAuth2" }) - authType?: ToolboxAuthType; + authType?: ToolboxAuthTypes; @doc("Additional configuration options for the tool") @sample(#{ options: #{ indexName: "products-index" } }) @@ -200,7 +233,7 @@ model ToolboxResource extends Resource { @doc("The tools contained in this toolbox") @sample(#{ tools: #[ - #{ id: "bing_grounding" }, + #{ id: "web_search" }, #{ id: "azure_ai_search", options: #{ indexName: "products-index" }, @@ -210,6 +243,12 @@ model ToolboxResource extends Resource { name: "github-copilot", target: "https://api.githubcopilot.com/mcp", authType: "OAuth2", + }, + #{ + id: "a2a_preview", + name: "research-agent", + description: "Delegates research tasks to a specialized agent", + target: "https://research-agent.example.com", } ], }) diff --git a/docs/src/content/docs/reference/README.md b/docs/src/content/docs/reference/README.md index 9f7103ec..9109baa6 100644 --- a/docs/src/content/docs/reference/README.md +++ b/docs/src/content/docs/reference/README.md @@ -273,6 +273,7 @@ classDiagram +string id +string name + +string description +string target +string authType +dictionary options diff --git a/docs/src/content/docs/reference/ToolboxResource.md b/docs/src/content/docs/reference/ToolboxResource.md index 2617c3f7..9f7d63ec 100644 --- a/docs/src/content/docs/reference/ToolboxResource.md +++ b/docs/src/content/docs/reference/ToolboxResource.md @@ -33,6 +33,7 @@ classDiagram class ToolboxTool { +string id +string name + +string description +string target +string authType +dictionary options @@ -46,7 +47,7 @@ classDiagram kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -54,6 +55,10 @@ tools: name: github-copilot target: https://api.githubcopilot.com/mcp authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: https://research-agent.example.com ``` ## Properties diff --git a/docs/src/content/docs/reference/ToolboxTool.md b/docs/src/content/docs/reference/ToolboxTool.md index 89e395e8..714a81f4 100644 --- a/docs/src/content/docs/reference/ToolboxTool.md +++ b/docs/src/content/docs/reference/ToolboxTool.md @@ -5,8 +5,8 @@ slug: "reference/toolboxtool" --- Represents a tool definition within a toolbox. -Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) -or external (mcp, openapi) with connection details. +Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) +or external (mcp, openapi, a2a_preview) with connection details. ## Class Diagram @@ -24,6 +24,7 @@ classDiagram +string id +string name + +string description +string target +string authType +dictionary options @@ -33,8 +34,9 @@ classDiagram ## Yaml Example ```yaml -id: bing_grounding +id: web_search name: my-search-tool +description: Searches the web for up-to-date information target: https://api.githubcopilot.com/mcp authType: OAuth2 options: @@ -45,8 +47,9 @@ options: | Name | Type | Description | | ---- | ---- | ----------- | -| id | string | The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') | +| id | string | The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') | | name | string | Optional display name for the tool | -| target | string | Target endpoint URL for external tools (e.g., MCP server URL) | +| description | string | Human-readable description of the tool's capabilities | +| target | string | Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL) | | authType | string | Authentication type for the tool connection | | options | dictionary | Additional configuration options for the tool | diff --git a/examples/toolbox/toolbox_manifest.yaml b/examples/toolbox/toolbox_manifest.yaml index 9bcef1af..75df6bb5 100644 --- a/examples/toolbox/toolbox_manifest.yaml +++ b/examples/toolbox/toolbox_manifest.yaml @@ -28,7 +28,7 @@ resources: name: platform-tools description: Shared platform tools for support workflows tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: support-docs-index @@ -41,6 +41,10 @@ resources: options: clientId: "{{ github_client_id }}" clientSecret: "{{ github_client_secret }}" + - id: a2a_preview + name: research-agent + description: Delegates deep research tasks to a specialized agent + target: https://research-agent.internal.example.com parameters: model_name: diff --git a/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs b/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs index 36bae104..ec998cd4 100644 --- a/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs +++ b/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs @@ -15,7 +15,7 @@ public void LoadYamlInput() kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -23,6 +23,10 @@ public void LoadYamlInput() name: github-copilot target: "https://api.githubcopilot.com/mcp" authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: "https://research-agent.example.com" """; @@ -42,7 +46,7 @@ public void LoadJsonInput() "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -55,6 +59,12 @@ public void LoadJsonInput() "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -76,7 +86,7 @@ public void RoundtripJson() "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -89,6 +99,12 @@ public void RoundtripJson() "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -114,7 +130,7 @@ public void RoundtripYaml() kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -122,6 +138,10 @@ public void RoundtripYaml() name: github-copilot target: "https://api.githubcopilot.com/mcp" authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: "https://research-agent.example.com" """; @@ -146,7 +166,7 @@ public void ToJsonProducesValidJson() "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -159,6 +179,12 @@ public void ToJsonProducesValidJson() "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -179,7 +205,7 @@ public void ToYamlProducesValidYaml() kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -187,6 +213,10 @@ public void ToYamlProducesValidYaml() name: github-copilot target: "https://api.githubcopilot.com/mcp" authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: "https://research-agent.example.com" """; diff --git a/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs b/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs index 6e8883ad..bbccb13d 100644 --- a/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs +++ b/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs @@ -12,8 +12,9 @@ public class ToolboxToolConversionTests public void LoadYamlInput() { string yamlData = """ -id: bing_grounding +id: web_search name: my-search-tool +description: Searches the web for up-to-date information target: "https://api.githubcopilot.com/mcp" authType: OAuth2 options: @@ -24,8 +25,9 @@ public void LoadYamlInput() var instance = ToolboxTool.FromYaml(yamlData); Assert.NotNull(instance); - Assert.Equal("bing_grounding", instance.Id); + Assert.Equal("web_search", instance.Id); Assert.Equal("my-search-tool", instance.Name); + Assert.Equal("Searches the web for up-to-date information", instance.Description); Assert.Equal("https://api.githubcopilot.com/mcp", instance.Target); Assert.Equal("OAuth2", instance.AuthType); } @@ -35,8 +37,9 @@ public void LoadJsonInput() { string jsonData = """ { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -47,8 +50,9 @@ public void LoadJsonInput() var instance = ToolboxTool.FromJson(jsonData); Assert.NotNull(instance); - Assert.Equal("bing_grounding", instance.Id); + Assert.Equal("web_search", instance.Id); Assert.Equal("my-search-tool", instance.Name); + Assert.Equal("Searches the web for up-to-date information", instance.Description); Assert.Equal("https://api.githubcopilot.com/mcp", instance.Target); Assert.Equal("OAuth2", instance.AuthType); } @@ -59,8 +63,9 @@ public void RoundtripJson() // Test that FromJson -> ToJson -> FromJson produces equivalent data string jsonData = """ { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -77,8 +82,9 @@ public void RoundtripJson() var reloaded = ToolboxTool.FromJson(json); Assert.NotNull(reloaded); - Assert.Equal("bing_grounding", reloaded.Id); + Assert.Equal("web_search", reloaded.Id); Assert.Equal("my-search-tool", reloaded.Name); + Assert.Equal("Searches the web for up-to-date information", reloaded.Description); Assert.Equal("https://api.githubcopilot.com/mcp", reloaded.Target); Assert.Equal("OAuth2", reloaded.AuthType); } @@ -88,8 +94,9 @@ public void RoundtripYaml() { // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data string yamlData = """ -id: bing_grounding +id: web_search name: my-search-tool +description: Searches the web for up-to-date information target: "https://api.githubcopilot.com/mcp" authType: OAuth2 options: @@ -105,8 +112,9 @@ public void RoundtripYaml() var reloaded = ToolboxTool.FromYaml(yaml); Assert.NotNull(reloaded); - Assert.Equal("bing_grounding", reloaded.Id); + Assert.Equal("web_search", reloaded.Id); Assert.Equal("my-search-tool", reloaded.Name); + Assert.Equal("Searches the web for up-to-date information", reloaded.Description); Assert.Equal("https://api.githubcopilot.com/mcp", reloaded.Target); Assert.Equal("OAuth2", reloaded.AuthType); } @@ -116,8 +124,9 @@ public void ToJsonProducesValidJson() { string jsonData = """ { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -138,8 +147,9 @@ public void ToJsonProducesValidJson() public void ToYamlProducesValidYaml() { string yamlData = """ -id: bing_grounding +id: web_search name: my-search-tool +description: Searches the web for up-to-date information target: "https://api.githubcopilot.com/mcp" authType: OAuth2 options: diff --git a/runtime/csharp/AgentSchema/ToolboxTool.cs b/runtime/csharp/AgentSchema/ToolboxTool.cs index 1ee270db..faf2743f 100644 --- a/runtime/csharp/AgentSchema/ToolboxTool.cs +++ b/runtime/csharp/AgentSchema/ToolboxTool.cs @@ -8,8 +8,8 @@ namespace AgentSchema; /// /// Represents a tool definition within a toolbox. -/// Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) -/// or external (mcp, openapi) with connection details. +/// Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) +/// or external (mcp, openapi, a2a_preview) with connection details. /// public class ToolboxTool { @@ -28,7 +28,7 @@ public ToolboxTool() #pragma warning restore CS8618 /// - /// The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + /// The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') /// public string Id { get; set; } = string.Empty; @@ -38,7 +38,12 @@ public ToolboxTool() public string? Name { get; set; } /// - /// Target endpoint URL for external tools (e.g., MCP server URL) + /// Human-readable description of the tool's capabilities + /// + public string? Description { get; set; } + + /// + /// Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL) /// public string? Target { get; set; } @@ -83,6 +88,11 @@ public static ToolboxTool Load(Dictionary data, LoadContext? co instance.Name = nameValue?.ToString()!; } + if (data.TryGetValue("description", out var descriptionValue) && descriptionValue is not null) + { + instance.Description = descriptionValue?.ToString()!; + } + if (data.TryGetValue("target", out var targetValue) && targetValue is not null) { instance.Target = targetValue?.ToString()!; @@ -138,6 +148,11 @@ public static ToolboxTool Load(Dictionary data, LoadContext? co result["name"] = obj.Name; } + if (obj.Description is not null) + { + result["description"] = obj.Description; + } + if (obj.Target is not null) { result["target"] = obj.Target; diff --git a/runtime/go/agentschema/toolbox_resource_test.go b/runtime/go/agentschema/toolbox_resource_test.go index cd60c915..6dc01c5a 100644 --- a/runtime/go/agentschema/toolbox_resource_test.go +++ b/runtime/go/agentschema/toolbox_resource_test.go @@ -19,7 +19,7 @@ func TestToolboxResourceLoadJSON(t *testing.T) { "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -32,6 +32,12 @@ func TestToolboxResourceLoadJSON(t *testing.T) { "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -60,7 +66,7 @@ func TestToolboxResourceLoadYAML(t *testing.T) { kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -68,6 +74,10 @@ tools: name: github-copilot target: "https://api.githubcopilot.com/mcp" authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: "https://research-agent.example.com" ` var data map[string]interface{} @@ -96,7 +106,7 @@ func TestToolboxResourceRoundtrip(t *testing.T) { "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -109,6 +119,12 @@ func TestToolboxResourceRoundtrip(t *testing.T) { "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -146,7 +162,7 @@ func TestToolboxResourceToJSON(t *testing.T) { "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -159,6 +175,12 @@ func TestToolboxResourceToJSON(t *testing.T) { "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -192,7 +214,7 @@ func TestToolboxResourceToYAML(t *testing.T) { "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -205,6 +227,12 @@ func TestToolboxResourceToYAML(t *testing.T) { "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } diff --git a/runtime/go/agentschema/toolbox_tool.go b/runtime/go/agentschema/toolbox_tool.go index c8ee263b..b01d3011 100644 --- a/runtime/go/agentschema/toolbox_tool.go +++ b/runtime/go/agentschema/toolbox_tool.go @@ -9,15 +9,16 @@ import ( ) // ToolboxTool represents Represents a tool definition within a toolbox. -// Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) -// or external (mcp, openapi) with connection details. +// Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) +// or external (mcp, openapi, a2a_preview) with connection details. type ToolboxTool struct { - Id string `json:"id" yaml:"id"` - Name *string `json:"name,omitempty" yaml:"name,omitempty"` - Target *string `json:"target,omitempty" yaml:"target,omitempty"` - AuthType *string `json:"authType,omitempty" yaml:"authType,omitempty"` - Options map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` + Id string `json:"id" yaml:"id"` + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Target *string `json:"target,omitempty" yaml:"target,omitempty"` + AuthType *string `json:"authType,omitempty" yaml:"authType,omitempty"` + Options map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` } // LoadToolboxTool creates a ToolboxTool from a map[string]interface{} @@ -33,6 +34,10 @@ func LoadToolboxTool(data interface{}, ctx *LoadContext) (ToolboxTool, error) { v := val.(string) result.Name = &v } + if val, ok := m["description"]; ok && val != nil { + v := val.(string) + result.Description = &v + } if val, ok := m["target"]; ok && val != nil { v := val.(string) result.Target = &v @@ -58,6 +63,9 @@ func (obj *ToolboxTool) Save(ctx *SaveContext) map[string]interface{} { if obj.Name != nil { result["name"] = *obj.Name } + if obj.Description != nil { + result["description"] = *obj.Description + } if obj.Target != nil { result["target"] = *obj.Target } diff --git a/runtime/go/agentschema/toolbox_tool_test.go b/runtime/go/agentschema/toolbox_tool_test.go index 92e552c5..42cfbd23 100644 --- a/runtime/go/agentschema/toolbox_tool_test.go +++ b/runtime/go/agentschema/toolbox_tool_test.go @@ -15,8 +15,9 @@ import ( func TestToolboxToolLoadJSON(t *testing.T) { jsonData := ` { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -34,12 +35,15 @@ func TestToolboxToolLoadJSON(t *testing.T) { if err != nil { t.Fatalf("Failed to load ToolboxTool: %v", err) } - if instance.Id != "bing_grounding" { - t.Errorf(`Expected Id to be "bing_grounding", got %v`, instance.Id) + if instance.Id != "web_search" { + t.Errorf(`Expected Id to be "web_search", got %v`, instance.Id) } if instance.Name == nil || *instance.Name != "my-search-tool" { t.Errorf(`Expected Name to be "my-search-tool", got %v`, instance.Name) } + if instance.Description == nil || *instance.Description != "Searches the web for up-to-date information" { + t.Errorf(`Expected Description to be "Searches the web for up-to-date information", got %v`, instance.Description) + } if instance.Target == nil || *instance.Target != "https://api.githubcopilot.com/mcp" { t.Errorf(`Expected Target to be "https://api.githubcopilot.com/mcp", got %v`, instance.Target) } @@ -51,8 +55,9 @@ func TestToolboxToolLoadJSON(t *testing.T) { // TestToolboxToolLoadYAML tests loading ToolboxTool from YAML func TestToolboxToolLoadYAML(t *testing.T) { yamlData := ` -id: bing_grounding +id: web_search name: my-search-tool +description: Searches the web for up-to-date information target: "https://api.githubcopilot.com/mcp" authType: OAuth2 options: @@ -69,12 +74,15 @@ options: if err != nil { t.Fatalf("Failed to load ToolboxTool: %v", err) } - if instance.Id != "bing_grounding" { - t.Errorf(`Expected Id to be "bing_grounding", got %v`, instance.Id) + if instance.Id != "web_search" { + t.Errorf(`Expected Id to be "web_search", got %v`, instance.Id) } if instance.Name == nil || *instance.Name != "my-search-tool" { t.Errorf(`Expected Name to be "my-search-tool", got %v`, instance.Name) } + if instance.Description == nil || *instance.Description != "Searches the web for up-to-date information" { + t.Errorf(`Expected Description to be "Searches the web for up-to-date information", got %v`, instance.Description) + } if instance.Target == nil || *instance.Target != "https://api.githubcopilot.com/mcp" { t.Errorf(`Expected Target to be "https://api.githubcopilot.com/mcp", got %v`, instance.Target) } @@ -87,8 +95,9 @@ options: func TestToolboxToolRoundtrip(t *testing.T) { jsonData := ` { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -113,12 +122,15 @@ func TestToolboxToolRoundtrip(t *testing.T) { if err != nil { t.Fatalf("Failed to reload ToolboxTool: %v", err) } - if reloaded.Id != "bing_grounding" { - t.Errorf(`Expected Id to be "bing_grounding", got %v`, reloaded.Id) + if reloaded.Id != "web_search" { + t.Errorf(`Expected Id to be "web_search", got %v`, reloaded.Id) } if reloaded.Name == nil || *reloaded.Name != "my-search-tool" { t.Errorf(`Expected Name to be "my-search-tool", got %v`, reloaded.Name) } + if reloaded.Description == nil || *reloaded.Description != "Searches the web for up-to-date information" { + t.Errorf(`Expected Description to be "Searches the web for up-to-date information", got %v`, reloaded.Description) + } if reloaded.Target == nil || *reloaded.Target != "https://api.githubcopilot.com/mcp" { t.Errorf(`Expected Target to be "https://api.githubcopilot.com/mcp", got %v`, reloaded.Target) } @@ -131,8 +143,9 @@ func TestToolboxToolRoundtrip(t *testing.T) { func TestToolboxToolToJSON(t *testing.T) { jsonData := ` { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -165,8 +178,9 @@ func TestToolboxToolToJSON(t *testing.T) { func TestToolboxToolToYAML(t *testing.T) { jsonData := ` { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { diff --git a/runtime/python/agentschema/src/agentschema/_ToolboxTool.py b/runtime/python/agentschema/src/agentschema/_ToolboxTool.py index edac6e27..d18eb2a2 100644 --- a/runtime/python/agentschema/src/agentschema/_ToolboxTool.py +++ b/runtime/python/agentschema/src/agentschema/_ToolboxTool.py @@ -13,17 +13,19 @@ @dataclass class ToolboxTool: """Represents a tool definition within a toolbox. - Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) - or external (mcp, openapi) with connection details. + Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) + or external (mcp, openapi, a2a_preview) with connection details. Attributes ---------- id : str - The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') name : Optional[str] Optional display name for the tool + description : Optional[str] + Human-readable description of the tool's capabilities target : Optional[str] - Target endpoint URL for external tools (e.g., MCP server URL) + Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL) authType : Optional[str] Authentication type for the tool connection options : Optional[dict[str, Any]] @@ -34,6 +36,7 @@ class ToolboxTool: id: str = field(default="") name: Optional[str] = None + description: Optional[str] = None target: Optional[str] = None authType: Optional[str] = None options: Optional[dict[str, Any]] = None @@ -62,6 +65,8 @@ def load(data: Any, context: Optional[LoadContext] = None) -> "ToolboxTool": instance.id = data["id"] if data is not None and "name" in data: instance.name = data["name"] + if data is not None and "description" in data: + instance.description = data["description"] if data is not None and "target" in data: instance.target = data["target"] if data is not None and "authType" in data: @@ -90,6 +95,8 @@ def save(self, context: Optional[SaveContext] = None) -> dict[str, Any]: result["id"] = obj.id if obj.name is not None: result["name"] = obj.name + if obj.description is not None: + result["description"] = obj.description if obj.target is not None: result["target"] = obj.target if obj.authType is not None: diff --git a/runtime/python/agentschema/tests/test_toolbox_resource.py b/runtime/python/agentschema/tests/test_toolbox_resource.py index 325fe705..5efdf44c 100644 --- a/runtime/python/agentschema/tests/test_toolbox_resource.py +++ b/runtime/python/agentschema/tests/test_toolbox_resource.py @@ -11,7 +11,7 @@ def test_load_json_toolboxresource(): "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -24,6 +24,12 @@ def test_load_json_toolboxresource(): "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -40,7 +46,7 @@ def test_load_yaml_toolboxresource(): kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -48,6 +54,10 @@ def test_load_yaml_toolboxresource(): name: github-copilot target: "https://api.githubcopilot.com/mcp" authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: "https://research-agent.example.com" """ data = yaml.load(yaml_data, Loader=yaml.FullLoader) @@ -65,7 +75,7 @@ def test_roundtrip_json_toolboxresource(): "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -78,6 +88,12 @@ def test_roundtrip_json_toolboxresource(): "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -99,7 +115,7 @@ def test_to_json_toolboxresource(): "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -112,6 +128,12 @@ def test_to_json_toolboxresource(): "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -132,7 +154,7 @@ def test_to_yaml_toolboxresource(): "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -145,6 +167,12 @@ def test_to_yaml_toolboxresource(): "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } diff --git a/runtime/python/agentschema/tests/test_toolbox_tool.py b/runtime/python/agentschema/tests/test_toolbox_tool.py index 61546c51..bae997ad 100644 --- a/runtime/python/agentschema/tests/test_toolbox_tool.py +++ b/runtime/python/agentschema/tests/test_toolbox_tool.py @@ -7,8 +7,9 @@ def test_load_json_toolboxtool(): json_data = r""" { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -19,16 +20,18 @@ def test_load_json_toolboxtool(): data = json.loads(json_data, strict=False) instance = ToolboxTool.load(data) assert instance is not None - assert instance.id == "bing_grounding" + assert instance.id == "web_search" assert instance.name == "my-search-tool" + assert instance.description == "Searches the web for up-to-date information" assert instance.target == "https://api.githubcopilot.com/mcp" assert instance.authType == "OAuth2" def test_load_yaml_toolboxtool(): yaml_data = r""" - id: bing_grounding + id: web_search name: my-search-tool + description: Searches the web for up-to-date information target: "https://api.githubcopilot.com/mcp" authType: OAuth2 options: @@ -38,8 +41,9 @@ def test_load_yaml_toolboxtool(): data = yaml.load(yaml_data, Loader=yaml.FullLoader) instance = ToolboxTool.load(data) assert instance is not None - assert instance.id == "bing_grounding" + assert instance.id == "web_search" assert instance.name == "my-search-tool" + assert instance.description == "Searches the web for up-to-date information" assert instance.target == "https://api.githubcopilot.com/mcp" assert instance.authType == "OAuth2" @@ -48,8 +52,9 @@ def test_roundtrip_json_toolboxtool(): """Test that load -> save -> load produces equivalent data.""" json_data = r""" { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -62,8 +67,9 @@ def test_roundtrip_json_toolboxtool(): saved_data = instance.save() reloaded = ToolboxTool.load(saved_data) assert reloaded is not None - assert reloaded.id == "bing_grounding" + assert reloaded.id == "web_search" assert reloaded.name == "my-search-tool" + assert reloaded.description == "Searches the web for up-to-date information" assert reloaded.target == "https://api.githubcopilot.com/mcp" assert reloaded.authType == "OAuth2" @@ -72,8 +78,9 @@ def test_to_json_toolboxtool(): """Test that to_json produces valid JSON.""" json_data = r""" { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -93,8 +100,9 @@ def test_to_yaml_toolboxtool(): """Test that to_yaml produces valid YAML.""" json_data = r""" { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { diff --git a/runtime/rust/agentschema/src/toolbox_tool.rs b/runtime/rust/agentschema/src/toolbox_tool.rs index 0be29e7b..2c559161 100644 --- a/runtime/rust/agentschema/src/toolbox_tool.rs +++ b/runtime/rust/agentschema/src/toolbox_tool.rs @@ -1,13 +1,15 @@ // Code generated by AgentSchema emitter; DO NOT EDIT. -/// Represents a tool definition within a toolbox. Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) or external (mcp, openapi) with connection details. +/// Represents a tool definition within a toolbox. Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) or external (mcp, openapi, a2a_preview) with connection details. #[derive(Debug, Clone, Default)] pub struct ToolboxTool { - /// The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + /// The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') pub id: String, /// Optional display name for the tool pub name: Option, - /// Target endpoint URL for external tools (e.g., MCP server URL) + /// Human-readable description of the tool's capabilities + pub description: Option, + /// Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL) pub target: Option, /// Authentication type for the tool connection pub auth_type: Option, @@ -45,6 +47,10 @@ impl ToolboxTool { .get("name") .and_then(|v| v.as_str()) .map(|s| s.to_string()), + description: value + .get("description") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), target: value .get("target") .and_then(|v| v.as_str()) @@ -69,6 +75,12 @@ impl ToolboxTool { if let Some(ref val) = self.name { result.insert("name".to_string(), serde_json::Value::String(val.clone())); } + if let Some(ref val) = self.description { + result.insert( + "description".to_string(), + serde_json::Value::String(val.clone()), + ); + } if let Some(ref val) = self.target { result.insert("target".to_string(), serde_json::Value::String(val.clone())); } diff --git a/runtime/rust/agentschema/tests/toolbox_resource_test.rs b/runtime/rust/agentschema/tests/toolbox_resource_test.rs index d07c3346..2007fdb8 100644 --- a/runtime/rust/agentschema/tests/toolbox_resource_test.rs +++ b/runtime/rust/agentschema/tests/toolbox_resource_test.rs @@ -10,7 +10,7 @@ fn test_toolbox_resource_load_json() { "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -23,6 +23,12 @@ fn test_toolbox_resource_load_json() { "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } @@ -51,7 +57,7 @@ fn test_toolbox_resource_load_yaml() { kind: toolbox description: Shared platform tools tools: - - id: bing_grounding + - id: web_search - id: azure_ai_search options: indexName: products-index @@ -59,6 +65,10 @@ tools: name: github-copilot target: "https://api.githubcopilot.com/mcp" authType: OAuth2 + - id: a2a_preview + name: research-agent + description: Delegates research tasks to a specialized agent + target: "https://research-agent.example.com" "####; let result = ToolboxResource::from_yaml(yaml); @@ -83,7 +93,7 @@ fn test_toolbox_resource_roundtrip() { "description": "Shared platform tools", "tools": [ { - "id": "bing_grounding" + "id": "web_search" }, { "id": "azure_ai_search", @@ -96,6 +106,12 @@ fn test_toolbox_resource_roundtrip() { "name": "github-copilot", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2" + }, + { + "id": "a2a_preview", + "name": "research-agent", + "description": "Delegates research tasks to a specialized agent", + "target": "https://research-agent.example.com" } ] } diff --git a/runtime/rust/agentschema/tests/toolbox_tool_test.rs b/runtime/rust/agentschema/tests/toolbox_tool_test.rs index fed92e77..bd106501 100644 --- a/runtime/rust/agentschema/tests/toolbox_tool_test.rs +++ b/runtime/rust/agentschema/tests/toolbox_tool_test.rs @@ -6,8 +6,9 @@ use agentschema::ToolboxTool; fn test_toolbox_tool_load_json() { let json = r####" { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { @@ -22,9 +23,17 @@ fn test_toolbox_tool_load_json() { result.err() ); let instance = result.unwrap(); - assert_eq!(instance.id, "bing_grounding"); + assert_eq!(instance.id, "web_search"); assert!(instance.name.is_some(), "Expected name to be Some"); assert_eq!(instance.name.as_ref().unwrap(), &"my-search-tool"); + assert!( + instance.description.is_some(), + "Expected description to be Some" + ); + assert_eq!( + instance.description.as_ref().unwrap(), + &"Searches the web for up-to-date information" + ); assert!(instance.target.is_some(), "Expected target to be Some"); assert_eq!( instance.target.as_ref().unwrap(), @@ -40,8 +49,9 @@ fn test_toolbox_tool_load_json() { #[test] fn test_toolbox_tool_load_yaml() { let yaml = r####" -id: bing_grounding +id: web_search name: my-search-tool +description: Searches the web for up-to-date information target: "https://api.githubcopilot.com/mcp" authType: OAuth2 options: @@ -55,8 +65,12 @@ options: result.err() ); let instance = result.unwrap(); - assert_eq!(instance.id, "bing_grounding"); + assert_eq!(instance.id, "web_search"); assert!(instance.name.is_some(), "Expected name to be Some"); + assert!( + instance.description.is_some(), + "Expected description to be Some" + ); assert!(instance.target.is_some(), "Expected target to be Some"); assert!( instance.auth_type.is_some(), @@ -68,8 +82,9 @@ options: fn test_toolbox_tool_roundtrip() { let json = r####" { - "id": "bing_grounding", + "id": "web_search", "name": "my-search-tool", + "description": "Searches the web for up-to-date information", "target": "https://api.githubcopilot.com/mcp", "authType": "OAuth2", "options": { diff --git a/runtime/typescript/agentschema/src/toolbox-tool.ts b/runtime/typescript/agentschema/src/toolbox-tool.ts index 0d8a41e2..9395cdb1 100644 --- a/runtime/typescript/agentschema/src/toolbox-tool.ts +++ b/runtime/typescript/agentschema/src/toolbox-tool.ts @@ -5,8 +5,8 @@ import { LoadContext, SaveContext } from "./context"; /** * Represents a tool definition within a toolbox. - * Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) - * or external (mcp, openapi) with connection details. + * Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) + * or external (mcp, openapi, a2a_preview) with connection details. * */ export class ToolboxTool { @@ -16,7 +16,7 @@ export class ToolboxTool { static readonly shorthandProperty: string | undefined = undefined; /** - * The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + * The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') */ id: string = ""; @@ -26,7 +26,12 @@ export class ToolboxTool { name?: string | undefined; /** - * Target endpoint URL for external tools (e.g., MCP server URL) + * Human-readable description of the tool's capabilities + */ + description?: string | undefined; + + /** + * Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL) */ target?: string | undefined; @@ -50,6 +55,10 @@ export class ToolboxTool { this.name = init.name; } + if (init?.description !== undefined) { + this.description = init.description; + } + if (init?.target !== undefined) { this.target = init.target; } @@ -87,6 +96,10 @@ export class ToolboxTool { instance.name = String(data["name"]); } + if (data["description"] !== undefined && data["description"] !== null) { + instance.description = String(data["description"]); + } + if (data["target"] !== undefined && data["target"] !== null) { instance.target = String(data["target"]); } @@ -127,6 +140,10 @@ export class ToolboxTool { result["name"] = obj.name; } + if (obj.description !== undefined && obj.description !== null) { + result["description"] = obj.description; + } + if (obj.target !== undefined && obj.target !== null) { result["target"] = obj.target; } diff --git a/runtime/typescript/agentschema/tests/toolbox-resource.test.ts b/runtime/typescript/agentschema/tests/toolbox-resource.test.ts index fac37f6c..3c841745 100644 --- a/runtime/typescript/agentschema/tests/toolbox-resource.test.ts +++ b/runtime/typescript/agentschema/tests/toolbox-resource.test.ts @@ -18,7 +18,7 @@ describe("ToolboxResource", () => { describe("JSON serialization", () => { it("should load from JSON - example 1", () => { - const json = `{\n "kind": "toolbox",\n "description": "Shared platform tools",\n "tools": [\n {\n "id": "bing_grounding"\n },\n {\n "id": "azure_ai_search",\n "options": {\n "indexName": "products-index"\n }\n },\n {\n "id": "mcp",\n "name": "github-copilot",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2"\n }\n ]\n}`; + const json = `{\n "kind": "toolbox",\n "description": "Shared platform tools",\n "tools": [\n {\n "id": "web_search"\n },\n {\n "id": "azure_ai_search",\n "options": {\n "indexName": "products-index"\n }\n },\n {\n "id": "mcp",\n "name": "github-copilot",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2"\n },\n {\n "id": "a2a_preview",\n "name": "research-agent",\n "description": "Delegates research tasks to a specialized agent",\n "target": "https://research-agent.example.com"\n }\n ]\n}`; const instance = ToolboxResource.fromJson(json); expect(instance).toBeDefined(); @@ -28,7 +28,7 @@ describe("ToolboxResource", () => { }); it("should round-trip JSON - example 1", () => { - const json = `{\n "kind": "toolbox",\n "description": "Shared platform tools",\n "tools": [\n {\n "id": "bing_grounding"\n },\n {\n "id": "azure_ai_search",\n "options": {\n "indexName": "products-index"\n }\n },\n {\n "id": "mcp",\n "name": "github-copilot",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2"\n }\n ]\n}`; + const json = `{\n "kind": "toolbox",\n "description": "Shared platform tools",\n "tools": [\n {\n "id": "web_search"\n },\n {\n "id": "azure_ai_search",\n "options": {\n "indexName": "products-index"\n }\n },\n {\n "id": "mcp",\n "name": "github-copilot",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2"\n },\n {\n "id": "a2a_preview",\n "name": "research-agent",\n "description": "Delegates research tasks to a specialized agent",\n "target": "https://research-agent.example.com"\n }\n ]\n}`; const instance = ToolboxResource.fromJson(json); const output = instance.toJson(); const reloaded = ToolboxResource.fromJson(output); @@ -41,7 +41,7 @@ describe("ToolboxResource", () => { describe("YAML serialization", () => { it("should load from YAML - example 1", () => { - const yaml = `kind: toolbox\ndescription: Shared platform tools\ntools:\n - id: bing_grounding\n - id: azure_ai_search\n options:\n indexName: products-index\n - id: mcp\n name: github-copilot\n target: "https://api.githubcopilot.com/mcp"\n authType: OAuth2\n`; + const yaml = `kind: toolbox\ndescription: Shared platform tools\ntools:\n - id: web_search\n - id: azure_ai_search\n options:\n indexName: products-index\n - id: mcp\n name: github-copilot\n target: "https://api.githubcopilot.com/mcp"\n authType: OAuth2\n - id: a2a_preview\n name: research-agent\n description: Delegates research tasks to a specialized agent\n target: "https://research-agent.example.com"\n`; const instance = ToolboxResource.fromYaml(yaml); expect(instance).toBeDefined(); @@ -51,7 +51,7 @@ describe("ToolboxResource", () => { }); it("should round-trip YAML - example 1", () => { - const yaml = `kind: toolbox\ndescription: Shared platform tools\ntools:\n - id: bing_grounding\n - id: azure_ai_search\n options:\n indexName: products-index\n - id: mcp\n name: github-copilot\n target: "https://api.githubcopilot.com/mcp"\n authType: OAuth2\n`; + const yaml = `kind: toolbox\ndescription: Shared platform tools\ntools:\n - id: web_search\n - id: azure_ai_search\n options:\n indexName: products-index\n - id: mcp\n name: github-copilot\n target: "https://api.githubcopilot.com/mcp"\n authType: OAuth2\n - id: a2a_preview\n name: research-agent\n description: Delegates research tasks to a specialized agent\n target: "https://research-agent.example.com"\n`; const instance = ToolboxResource.fromYaml(yaml); const output = instance.toYaml(); const reloaded = ToolboxResource.fromYaml(output); diff --git a/runtime/typescript/agentschema/tests/toolbox-tool.test.ts b/runtime/typescript/agentschema/tests/toolbox-tool.test.ts index ba55bb63..a49442fc 100644 --- a/runtime/typescript/agentschema/tests/toolbox-tool.test.ts +++ b/runtime/typescript/agentschema/tests/toolbox-tool.test.ts @@ -18,21 +18,23 @@ describe("ToolboxTool", () => { describe("JSON serialization", () => { it("should load from JSON - example 1", () => { - const json = `{\n "id": "bing_grounding",\n "name": "my-search-tool",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2",\n "options": {\n "indexName": "products-index"\n }\n}`; + const json = `{\n "id": "web_search",\n "name": "my-search-tool",\n "description": "Searches the web for up-to-date information",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2",\n "options": {\n "indexName": "products-index"\n }\n}`; const instance = ToolboxTool.fromJson(json); expect(instance).toBeDefined(); - expect(instance.id).toEqual("bing_grounding"); + expect(instance.id).toEqual("web_search"); expect(instance.name).toEqual("my-search-tool"); + expect(instance.description).toEqual("Searches the web for up-to-date information"); + expect(instance.target).toEqual("https://api.githubcopilot.com/mcp"); expect(instance.authType).toEqual("OAuth2"); }); it("should round-trip JSON - example 1", () => { - const json = `{\n "id": "bing_grounding",\n "name": "my-search-tool",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2",\n "options": {\n "indexName": "products-index"\n }\n}`; + const json = `{\n "id": "web_search",\n "name": "my-search-tool",\n "description": "Searches the web for up-to-date information",\n "target": "https://api.githubcopilot.com/mcp",\n "authType": "OAuth2",\n "options": {\n "indexName": "products-index"\n }\n}`; const instance = ToolboxTool.fromJson(json); const output = instance.toJson(); const reloaded = ToolboxTool.fromJson(output); @@ -41,6 +43,8 @@ describe("ToolboxTool", () => { expect(reloaded.name).toEqual(instance.name); + expect(reloaded.description).toEqual(instance.description); + expect(reloaded.target).toEqual(instance.target); expect(reloaded.authType).toEqual(instance.authType); @@ -49,21 +53,23 @@ describe("ToolboxTool", () => { describe("YAML serialization", () => { it("should load from YAML - example 1", () => { - const yaml = `id: bing_grounding\nname: my-search-tool\ntarget: "https://api.githubcopilot.com/mcp"\nauthType: OAuth2\noptions:\n indexName: products-index\n`; + const yaml = `id: web_search\nname: my-search-tool\ndescription: Searches the web for up-to-date information\ntarget: "https://api.githubcopilot.com/mcp"\nauthType: OAuth2\noptions:\n indexName: products-index\n`; const instance = ToolboxTool.fromYaml(yaml); expect(instance).toBeDefined(); - expect(instance.id).toEqual("bing_grounding"); + expect(instance.id).toEqual("web_search"); expect(instance.name).toEqual("my-search-tool"); + expect(instance.description).toEqual("Searches the web for up-to-date information"); + expect(instance.target).toEqual("https://api.githubcopilot.com/mcp"); expect(instance.authType).toEqual("OAuth2"); }); it("should round-trip YAML - example 1", () => { - const yaml = `id: bing_grounding\nname: my-search-tool\ntarget: "https://api.githubcopilot.com/mcp"\nauthType: OAuth2\noptions:\n indexName: products-index\n`; + const yaml = `id: web_search\nname: my-search-tool\ndescription: Searches the web for up-to-date information\ntarget: "https://api.githubcopilot.com/mcp"\nauthType: OAuth2\noptions:\n indexName: products-index\n`; const instance = ToolboxTool.fromYaml(yaml); const output = instance.toYaml(); const reloaded = ToolboxTool.fromYaml(output); @@ -72,6 +78,8 @@ describe("ToolboxTool", () => { expect(reloaded.name).toEqual(instance.name); + expect(reloaded.description).toEqual(instance.description); + expect(reloaded.target).toEqual(instance.target); expect(reloaded.authType).toEqual(instance.authType); diff --git a/schemas/v1.0/ToolboxAuthTypes.yaml b/schemas/v1.0/ToolboxAuthTypes.yaml new file mode 100644 index 00000000..bd582763 --- /dev/null +++ b/schemas/v1.0/ToolboxAuthTypes.yaml @@ -0,0 +1,20 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: ToolboxAuthTypes.yaml +anyOf: + - type: string + const: CustomKeys + description: Custom API key or PAT-based authentication + - type: string + const: OAuth2 + description: OAuth 2.0 with identity passthrough + - type: string + const: UserEntraToken + description: Microsoft Entra ID user token + - type: string + const: ProjectManagedIdentity + description: Foundry project managed identity + - type: string + const: AgenticIdentityToken + description: Agentic identity token (preview) + - type: string +description: Authentication types for toolbox tool connections diff --git a/schemas/v1.0/ToolboxTool.yaml b/schemas/v1.0/ToolboxTool.yaml index f2e2673e..c5f54aaa 100644 --- a/schemas/v1.0/ToolboxTool.yaml +++ b/schemas/v1.0/ToolboxTool.yaml @@ -3,40 +3,19 @@ $id: ToolboxTool.yaml type: object properties: id: - anyOf: - - type: string - const: bing_grounding - - type: string - const: azure_ai_search - - type: string - const: code_interpreter - - type: string - const: file_search - - type: string - const: mcp - - type: string - const: openapi - - type: string - description: The tool type identifier (e.g., 'bing_grounding', 'azure_ai_search', 'mcp') + $ref: ToolboxToolTypes.yaml + description: The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') name: type: string description: Optional display name for the tool + description: + type: string + description: Human-readable description of the tool's capabilities target: type: string - description: Target endpoint URL for external tools (e.g., MCP server URL) + description: Target endpoint URL for external tools (e.g., MCP server URL, A2A agent URL) authType: - anyOf: - - type: string - const: CustomKeys - - type: string - const: OAuth2 - - type: string - const: UserEntraToken - - type: string - const: ProjectManagedIdentity - - type: string - const: AgenticIdentityToken - - type: string + $ref: ToolboxAuthTypes.yaml description: Authentication type for the tool connection options: $ref: RecordUnknown.yaml @@ -46,5 +25,5 @@ required: - id description: |- Represents a tool definition within a toolbox. - Tools can be Foundry-hosted (bing_grounding, azure_ai_search, etc.) - or external (mcp, openapi) with connection details. + Tools can be Foundry-hosted (web_search, azure_ai_search, etc.) + or external (mcp, openapi, a2a_preview) with connection details. diff --git a/schemas/v1.0/ToolboxToolTypes.yaml b/schemas/v1.0/ToolboxToolTypes.yaml new file mode 100644 index 00000000..3229900b --- /dev/null +++ b/schemas/v1.0/ToolboxToolTypes.yaml @@ -0,0 +1,26 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: ToolboxToolTypes.yaml +anyOf: + - type: string + const: mcp + description: Model Context Protocol server + - type: string + const: web_search + description: Web search via Bing grounding + - type: string + const: azure_ai_search + description: Azure AI Search index + - type: string + const: openapi + description: OpenAPI specification endpoint + - type: string + const: a2a_preview + description: Agent-to-agent delegation (preview) + - type: string + const: code_interpreter + description: Sandboxed Python code execution + - type: string + const: file_search + description: Vector store file search + - type: string +description: Known toolbox tool type identifiers