Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!-- payload-validation -->

```csharp
using Microsoft.Teams.Apps.HtmlWidget;

try
{
var message = HtmlWidgetHelpers.BuildHtmlWidgetMessage(
new HtmlWidgetPayload
{
Name = "Simple Widget",
Html = "<div>Hello from a widget</div>",
Domain = "https://teams.microsoft.com",
});
await context.Send(message, cancellationToken);
}
catch (ArgumentException ex)
{
// Thrown when Name or Html is empty, or Domain is not a valid https:// URL.
logger.LogError("Invalid widget payload: {Message}", ex.Message);
}
```

<!-- tool-error -->

```csharp
teams.OnWidgetCallTool(async (context, cancellationToken) =>
{
var toolName = context.Activity.Value?.Name ?? "unknown";

await Task.CompletedTask;
return HtmlWidgetCallToolResponse.FromError($"Unknown tool: {toolName}");
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!-- payload-validation -->

```python
from microsoft_teams.api.models.html_widget import HtmlWidgetPayload
from microsoft_teams.apps.utils.html_widget import build_html_widget_message

try:
message = build_html_widget_message(
HtmlWidgetPayload(
name="Simple Widget",
html="<div>Hello from a widget</div>",
domain="https://teams.microsoft.com",
)
)
await ctx.send(message)
except ValueError as err:
# Raised when name or html is empty, or domain is not a valid https:// URL.
logger.error("Invalid widget payload: %s", err)
```

<!-- tool-error -->

```python
from microsoft_teams.api.models.html_widget import (
HtmlWidgetCallToolResponse,
McpUiCallToolResult,
McpUiTextContent,
)

result = McpUiCallToolResult(
content=[McpUiTextContent(type="text", text=f"Unknown tool: {tool_name}")],
is_error=True,
)

return HtmlWidgetCallToolResponse(
response_type="htmlwidget/calltoolresult",
call_tool_result=result,
)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!-- payload-validation -->

```typescript
import { buildHtmlWidgetMessage } from '@microsoft/teams.apps';

try {
const message = buildHtmlWidgetMessage({
name: 'Simple Widget',
html: '<div>Hello from a widget</div>',
domain: 'https://teams.microsoft.com',
});
await send(message);
} catch (err) {
// Thrown when name or html is empty, or domain is not a valid https:// URL.
console.error(`Invalid widget payload: ${(err as Error).message}`);
}
```

<!-- tool-error -->

```typescript
import { IHtmlWidgetCallToolResponse } from '@microsoft/teams.api';

app.on('widget.callTool', async ({ activity }) => {
const { name } = activity.value;

const response: IHtmlWidgetCallToolResponse = {
responseType: 'htmlwidget/calltoolresult',
callToolResult: {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
},
};
return response;
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!-- handler-code -->

```csharp
using Microsoft.Teams.Apps.HtmlWidget;

teams.OnWidgetCallTool(async (context, cancellationToken) =>
{
var request = context.Activity.Value;
var toolName = request?.Name ?? "unknown";

var response = toolName switch
{
"getTime" => new HtmlWidgetCallToolResponse
{
CallToolResult = new McpUiCallToolResult
{
Content = [new McpUiCallToolResultContent { Text = DateTime.UtcNow.ToString("HH:mm:ss") }],
StructuredContent = new { time = DateTime.UtcNow.ToString("o") },
}
},
_ => HtmlWidgetCallToolResponse.FromError($"Unknown tool: {toolName}"),
};

await Task.CompletedTask;
return response;
});
```

<!-- response-intro -->

Return an `HtmlWidgetCallToolResponse` whose `ResponseType` is `htmlwidget/calltoolresult` and whose `CallToolResult` holds the payload.
The result's `Content` is a list of content blocks; `StructuredContent` holds data the widget can render from; `IsError` signals a failure.
For the common text-only case, use the `FromText` and `FromError` factory methods.

<!-- response-code -->

```csharp
var response = new HtmlWidgetCallToolResponse
{
CallToolResult = new McpUiCallToolResult
{
Content = [new McpUiCallToolResultContent { Text = "Refreshed!" }],
StructuredContent = new { counter = 1, lastAction = "refresh" },
}
};

// Or, for a simple text result:
var quick = HtmlWidgetCallToolResponse.FromText("Refreshed!");
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!-- handler-code -->

```python
from typing import Any

from microsoft_teams.api import HtmlWidgetCallToolInvokeActivity
from microsoft_teams.api.models.html_widget import (
HtmlWidgetCallToolResponse,
McpUiCallToolResult,
McpUiTextContent,
)
from microsoft_teams.apps import ActivityContext


@app.on_widget_call_tool
async def handle_widget_call_tool(
ctx: ActivityContext[HtmlWidgetCallToolInvokeActivity],
) -> HtmlWidgetCallToolResponse:
tool_name = ctx.activity.value.name
args: dict[str, Any] = ctx.activity.value.arguments or {}

if tool_name == "getTime":
from datetime import datetime, timezone

now = datetime.now(tz=timezone.utc)
result = McpUiCallToolResult(
content=[McpUiTextContent(type="text", text=now.strftime("%H:%M:%S"))],
structured_content={"time": now.isoformat()},
is_error=False,
)
else:
result = McpUiCallToolResult(
content=[McpUiTextContent(type="text", text=f"Unknown tool: {tool_name}")],
is_error=True,
)

return HtmlWidgetCallToolResponse(
response_type="htmlwidget/calltoolresult",
call_tool_result=result,
)
```

<!-- response-intro -->

Return an `HtmlWidgetCallToolResponse` with `response_type` set to `htmlwidget/calltoolresult` and a `call_tool_result` payload.
The result's `content` is a list of content blocks; `structured_content` holds data the widget can render from; `is_error` signals a failure.

<!-- response-code -->

```python
result = McpUiCallToolResult(
content=[McpUiTextContent(type="text", text="Refreshed!")],
structured_content={"counter": 1, "lastAction": "refresh"},
is_error=False,
)

return HtmlWidgetCallToolResponse(
response_type="htmlwidget/calltoolresult",
call_tool_result=result,
)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!-- handler-code -->

```typescript
import { IHtmlWidgetCallToolResponse, IMcpUiCallToolResult } from '@microsoft/teams.api';

app.on('widget.callTool', async ({ activity }) => {
const { name, arguments: args } = activity.value;

let callToolResult: IMcpUiCallToolResult;
switch (name) {
case 'getTime':
callToolResult = {
content: [{ type: 'text', text: new Date().toLocaleTimeString() }],
structuredContent: { time: new Date().toISOString() },
isError: false,
};
break;
default:
callToolResult = {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
break;
}

const response: IHtmlWidgetCallToolResponse = {
responseType: 'htmlwidget/calltoolresult',
callToolResult,
};
return response;
});
```

<!-- response-intro -->

Return an `IHtmlWidgetCallToolResponse` with `responseType` set to `htmlwidget/calltoolresult` and a `callToolResult` payload.
The result's `content` is an array of content blocks; `structuredContent` holds data the widget can render from; `isError` signals a failure.

<!-- response-code -->

```typescript
const callToolResult: IMcpUiCallToolResult = {
content: [{ type: 'text', text: 'Refreshed!' }],
structuredContent: { counter: 1, lastAction: 'refresh' },
isError: false,
};

const response: IHtmlWidgetCallToolResponse = {
responseType: 'htmlwidget/calltoolresult',
callToolResult,
};
return response;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!-- declare-code -->

```csharp
using Microsoft.Teams.Apps.HtmlWidget;

// Attach a security policy to the payload. The build helper carries it through
// to the widget block; omit it and the SDK applies a restrictive default.
var message = HtmlWidgetHelpers.BuildHtmlWidgetMessage(
new HtmlWidgetPayload
{
Name = "Chart Widget",
Html = "<div id=\"chart\"></div>",
Domain = "https://teams.microsoft.com",
SecurityPolicy = new HtmlWidgetSecurityPolicy
{
ConnectDomains = ["https://api.contoso.com"],
ResourceDomains = ["'self'", "data:", "https://cdn.contoso.com"],
FrameDomains = [],
BaseUriDomains = [],
},
});

await context.Send(message, cancellationToken);
```

<!-- validate-code -->

```csharp
using Microsoft.Teams.Apps.HtmlWidget;

var html =
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Roboto\">" +
"<div style=\"font-family: Roboto, sans-serif;\">Validation demo</div>";

var policy = new HtmlWidgetSecurityPolicy
{
ConnectDomains = [],
ResourceDomains = ["'self'", "data:"],
FrameDomains = [],
BaseUriDomains = [],
};

// Run the audit only in development so it never executes in production.
var isDevelopment = builder.Environment.IsDevelopment();
if (isDevelopment)
{
foreach (var w in HtmlWidgetHelpers.ValidateSecurityPolicy(html, policy))
{
Console.WriteLine($"{w.Source}: {w.Url} is not in {w.PolicyField}");
}
}

// In development the audit above would warn that this HTML loads the Roboto
// stylesheet from fonts.googleapis.com and the font files from fonts.gstatic.com,
// so you would add both origins to ResourceDomains before shipping.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!-- declare-code -->

```python
from microsoft_teams.api.models.html_widget import HtmlWidgetPayload, HtmlWidgetSecurityPolicy
from microsoft_teams.apps.utils.html_widget import build_html_widget_message

# Attach a security policy to the payload. The build helper carries it through
# to the widget block; omit it and the SDK applies a restrictive default.
message = build_html_widget_message(
HtmlWidgetPayload(
name="Chart Widget",
html='<div id="chart"></div>',
domain="https://teams.microsoft.com",
security_policy=HtmlWidgetSecurityPolicy(
connect_domains=["https://api.contoso.com"],
resource_domains=["'self'", "data:", "https://cdn.contoso.com"],
frame_domains=[],
base_uri_domains=[],
),
)
)

await ctx.send(message)
```

<!-- validate-code -->

```python
import os

from microsoft_teams.api.models.html_widget import HtmlWidgetSecurityPolicy
from microsoft_teams.apps.utils.html_widget import validate_security_policy

html = (
'<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto">'
'<div style="font-family: Roboto, sans-serif;">Validation demo</div>'
)

policy = HtmlWidgetSecurityPolicy(
connect_domains=[],
resource_domains=["'self'", "data:"],
frame_domains=[],
base_uri_domains=[],
)

# Run the audit only in development so it never executes in production.
is_development = os.environ.get("ENV") != "production"
if is_development:
for w in validate_security_policy(html, policy):
print(f"{w.source}: {w.url} is not in {w.policy_field}")

# In development the audit above would warn that this HTML loads the Roboto
# stylesheet from fonts.googleapis.com and the font files from fonts.gstatic.com,
# so you would add both origins to resource_domains before shipping.
```
Loading
Loading