diff --git a/agentschema-emitter/lib/model/manifest.tsp b/agentschema-emitter/lib/model/manifest.tsp index 5c31bc98..e6ab549e 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,118 @@ alias Resources = Record | Named< "Name of the resource", #{ name: "my-resource" } >[]; + +@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 (web_search, azure_ai_search, etc.) + * or external (mcp, openapi, a2a_preview) with connection details. + */ +model ToolboxTool { + @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("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?: ToolboxAuthTypes; + + @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: "web_search" }, + #{ + id: "azure_ai_search", + options: #{ indexName: "products-index" }, + }, + #{ + id: "mcp", + 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", + } + ], + }) + 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..9109baa6 100644 --- a/docs/src/content/docs/reference/README.md +++ b/docs/src/content/docs/reference/README.md @@ -269,6 +269,21 @@ classDiagram +string id +dictionary options } + class ToolboxTool { + + +string id + +string name + +string description + +string target + +string authType + +dictionary options + } + class ToolboxResource { + + +string kind + +string description + +ToolboxTool[] tools + } class AgentManifest { +string name @@ -302,6 +317,7 @@ classDiagram McpServerApprovalMode <|-- McpServerToolSpecifyApprovalMode Resource <|-- ModelResource Resource <|-- ToolResource + Resource <|-- ToolboxResource AgentDefinition *-- PropertySchema AgentDefinition *-- PropertySchema Model *-- Connection @@ -326,6 +342,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..9f7d63ec --- /dev/null +++ b/docs/src/content/docs/reference/ToolboxResource.md @@ -0,0 +1,76 @@ +--- +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 description + +string target + +string authType + +dictionary options + } + ToolboxResource *-- ToolboxTool +``` + +## Yaml Example + +```yaml +kind: toolbox +description: Shared platform tools +tools: + - id: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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 + +| 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..714a81f4 --- /dev/null +++ b/docs/src/content/docs/reference/ToolboxTool.md @@ -0,0 +1,55 @@ +--- +title: "ToolboxTool" +description: "Documentation for the ToolboxTool type." +slug: "reference/toolboxtool" +--- + +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. + +## Class Diagram + +```mermaid +--- +title: ToolboxTool +config: + look: handDrawn + theme: colorful + class: + hideEmptyMembersBox: true +--- +classDiagram + class ToolboxTool { + + +string id + +string name + +string description + +string target + +string authType + +dictionary options + } +``` + +## Yaml Example + +```yaml +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: + indexName: products-index +``` + +## Properties + +| Name | Type | Description | +| ---- | ---- | ----------- | +| id | string | The tool type identifier (e.g., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') | +| name | string | Optional display name for the tool | +| 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 new file mode 100644 index 00000000..75df6bb5 --- /dev/null +++ b/examples/toolbox/toolbox_manifest.yaml @@ -0,0 +1,67 @@ +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: web_search + - 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 }}" + - 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: + 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..ec998cd4 --- /dev/null +++ b/runtime/csharp/AgentSchema.Tests/ToolboxResourceConversionTests.cs @@ -0,0 +1,231 @@ + +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: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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 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: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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 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: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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 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..bbccb13d --- /dev/null +++ b/runtime/csharp/AgentSchema.Tests/ToolboxToolConversionTests.cs @@ -0,0 +1,168 @@ + +using Xunit; + +#pragma warning disable IDE0130 +namespace AgentSchema; +#pragma warning restore IDE0130 + + +public class ToolboxToolConversionTests +{ + [Fact] + public void LoadYamlInput() + { + string yamlData = """ +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: + indexName: products-index + +"""; + + var instance = ToolboxTool.FromYaml(yamlData); + + Assert.NotNull(instance); + 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); + } + + [Fact] + public void LoadJsonInput() + { + string jsonData = """ +{ + "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": { + "indexName": "products-index" + } +} +"""; + + var instance = ToolboxTool.FromJson(jsonData); + Assert.NotNull(instance); + 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); + } + + [Fact] + public void RoundtripJson() + { + // Test that FromJson -> ToJson -> FromJson produces equivalent data + string jsonData = """ +{ + "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": { + "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("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); + } + + [Fact] + public void RoundtripYaml() + { + // Test that FromYaml -> ToYaml -> FromYaml produces equivalent data + string yamlData = """ +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: + 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("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); + } + + [Fact] + public void ToJsonProducesValidJson() + { + string jsonData = """ +{ + "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": { + "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: web_search +name: my-search-tool +description: Searches the web for up-to-date information +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..faf2743f --- /dev/null +++ b/runtime/csharp/AgentSchema/ToolboxTool.cs @@ -0,0 +1,235 @@ +// 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 (web_search, azure_ai_search, etc.) +/// or external (mcp, openapi, a2a_preview) 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., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') + /// + public string Id { get; set; } = string.Empty; + + /// + /// Optional display name for the tool + /// + public string? Name { get; set; } + + /// + /// 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; } + + /// + /// 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("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()!; + } + + 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.Description is not null) + { + result["description"] = obj.Description; + } + + 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..6dc01c5a --- /dev/null +++ b/runtime/go/agentschema/toolbox_resource_test.go @@ -0,0 +1,259 @@ +// 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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{} + 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: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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{} + 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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{} + 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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{} + 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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{} + 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..b01d3011 --- /dev/null +++ b/runtime/go/agentschema/toolbox_tool.go @@ -0,0 +1,122 @@ +// 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 (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"` + 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{} +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["description"]; ok && val != nil { + v := val.(string) + result.Description = &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.Description != nil { + result["description"] = *obj.Description + } + 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..42cfbd23 --- /dev/null +++ b/runtime/go/agentschema/toolbox_tool_test.go @@ -0,0 +1,210 @@ +// 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": "web_search", + "name": "my-search-tool", + "description": "Searches the web for up-to-date information", + "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 != "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) + } + 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: web_search +name: my-search-tool +description: Searches the web for up-to-date information +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 != "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) + } + 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": "web_search", + "name": "my-search-tool", + "description": "Searches the web for up-to-date information", + "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 != "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) + } + 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": "web_search", + "name": "my-search-tool", + "description": "Searches the web for up-to-date information", + "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": "web_search", + "name": "my-search-tool", + "description": "Searches the web for up-to-date information", + "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..d18eb2a2 --- /dev/null +++ b/runtime/python/agentschema/src/agentschema/_ToolboxTool.py @@ -0,0 +1,134 @@ +########################################## +# 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 (web_search, azure_ai_search, etc.) + or external (mcp, openapi, a2a_preview) with connection details. + + Attributes + ---------- + id : str + 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, A2A agent 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 + description: 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 "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: + 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.description is not None: + result["description"] = obj.description + 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..5efdf44c --- /dev/null +++ b/runtime/python/agentschema/tests/test_toolbox_resource.py @@ -0,0 +1,185 @@ +import json +import yaml + +from agentschema import ToolboxResource + + +def test_load_json_toolboxresource(): + json_data = r""" + { + "kind": "toolbox", + "description": "Shared platform tools", + "tools": [ + { + "id": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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 = 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: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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) + 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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" + } + ] + } + """ + 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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 = 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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 = 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..bae997ad --- /dev/null +++ b/runtime/python/agentschema/tests/test_toolbox_tool.py @@ -0,0 +1,118 @@ +import json +import yaml + +from agentschema import ToolboxTool + + +def test_load_json_toolboxtool(): + json_data = r""" + { + "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": { + "indexName": "products-index" + } + } + """ + data = json.loads(json_data, strict=False) + instance = ToolboxTool.load(data) + assert instance is not None + 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: web_search + name: my-search-tool + description: Searches the web for up-to-date information + 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 == "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_roundtrip_json_toolboxtool(): + """Test that load -> save -> load produces equivalent data.""" + json_data = r""" + { + "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": { + "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 == "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" + + +def test_to_json_toolboxtool(): + """Test that to_json produces valid JSON.""" + json_data = r""" + { + "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": { + "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": "web_search", + "name": "my-search-tool", + "description": "Searches the web for up-to-date information", + "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..2c559161 --- /dev/null +++ b/runtime/rust/agentschema/src/toolbox_tool.rs @@ -0,0 +1,113 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +/// 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., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') + pub id: String, + /// Optional display name for the tool + pub name: Option, + /// 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, + /// 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()), + 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()) + .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.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())); + } + 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..2007fdb8 --- /dev/null +++ b/runtime/rust/agentschema/tests/toolbox_resource_test.rs @@ -0,0 +1,128 @@ +// 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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_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: web_search + - id: azure_ai_search + options: + indexName: products-index + - id: mcp + 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); + 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": "web_search" + }, + { + "id": "azure_ai_search", + "options": { + "indexName": "products-index" + } + }, + { + "id": "mcp", + "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_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..bd106501 --- /dev/null +++ b/runtime/rust/agentschema/tests/toolbox_tool_test.rs @@ -0,0 +1,104 @@ +// Code generated by AgentSchema emitter; DO NOT EDIT. + +use agentschema::ToolboxTool; + +#[test] +fn test_toolbox_tool_load_json() { + let json = r####" +{ + "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": { + "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, "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(), + &"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: web_search +name: my-search-tool +description: Searches the web for up-to-date information +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, "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(), + "Expected auth_type to be Some" + ); +} + +#[test] +fn test_toolbox_tool_roundtrip() { + let json = r####" +{ + "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": { + "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..9395cdb1 --- /dev/null +++ b/runtime/typescript/agentschema/src/toolbox-tool.ts @@ -0,0 +1,213 @@ +// 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 (web_search, azure_ai_search, etc.) + * or external (mcp, openapi, a2a_preview) 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., 'web_search', 'azure_ai_search', 'mcp', 'a2a_preview') + */ + id: string = ""; + + /** + * Optional display name for the tool + */ + name?: string | undefined; + + /** + * 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; + + /** + * 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?.description !== undefined) { + this.description = init.description; + } + + 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["description"] !== undefined && data["description"] !== null) { + instance.description = String(data["description"]); + } + + 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.description !== undefined && obj.description !== null) { + result["description"] = obj.description; + } + + 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..3c841745 --- /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": "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(); + + 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": "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); + + 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: 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(); + + 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: 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); + + 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..a49442fc --- /dev/null +++ b/runtime/typescript/agentschema/tests/toolbox-tool.test.ts @@ -0,0 +1,103 @@ +// 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": "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("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": "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); + + expect(reloaded.id).toEqual(instance.id); + + expect(reloaded.name).toEqual(instance.name); + + expect(reloaded.description).toEqual(instance.description); + + 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: 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("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: 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); + + expect(reloaded.id).toEqual(instance.id); + + expect(reloaded.name).toEqual(instance.name); + + expect(reloaded.description).toEqual(instance.description); + + 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/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/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..c5f54aaa --- /dev/null +++ b/schemas/v1.0/ToolboxTool.yaml @@ -0,0 +1,29 @@ +$schema: https://json-schema.org/draft/2020-12/schema +$id: ToolboxTool.yaml +type: object +properties: + id: + $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, A2A agent URL) + authType: + $ref: ToolboxAuthTypes.yaml + 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 (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