> ## Documentation Index
> Fetch the complete documentation index at: https://ai.easyaf.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Ask the operator to open a URL

> When an action returns a connect URL, the harness shows it. After they finish in the browser, your waiter sends the real result.

Connected Accounts, device login, any hop that belongs in a **browser**: your action returns a Started ticket with a URL. The harness should show that URL, then wait until *your* callback says the grant finished.

Mark the action. Register a waiter. Tokens never go through the chat.

## Mark the URL

**Restier / controllers**

```csharp theme={null}
using Microsoft.OData.Mcp.Core.Vocabularies;

[BoundOperation(OperationType = OperationType.Action)]
[McpTask]
[McpElicitUrl("connectUri")]
public AuthorizationTicket Authorize(IntegrationConnection connection)
{
    return ticket;
}
```

**ModelBuilder**

```csharp theme={null}
builder.EntityType<IntegrationConnection>()
    .Action("Authorize")
    .HasMcpTask()
    .HasMcpElicitUrl("connectUri")
    .Returns<AuthorizationTicket>();
```

Keep returning one JSON ticket, for example:

```json theme={null}
{
  "status": "Started",
  "connectUri": "https://login.example.com/connect/...",
  "expiresIn": 600,
  "message": "Open the URL to finish authorization."
}
```

If you omit `[McpElicitUrl]`, the host looks for `connectUri`, `url`, `authorizationUrl`, or `verificationUri` on that JSON. Names that look like secrets (`token`, `password`, `code`) are ignored on purpose.

Pair it with `[McpTask]` / `HasMcpTask()` so the harness waits while the operator is in the browser.

## Wait until your callback fires

```csharp theme={null}
builder.Services.AddODataMcp(options => { }, mcp =>
{
    mcp.WaitForAnnotatedAction<AuthorizeWaiter>();
});
```

```csharp theme={null}
public sealed class AuthorizeWaiter : IMcpAnnotatedActionWaiter
{
    private readonly ConnectionSessions _sessions;

    public AuthorizeWaiter(ConnectionSessions sessions) => _sessions = sessions;

    public async Task<CallToolResult?> WaitAsync(
        McpAnnotatedActionContext context,
        CancellationToken cancellationToken)
    {
        var json = await _sessions.WaitAsync(context.BindingKey, cancellationToken);
        return new CallToolResult
        {
            Content = [new TextContentBlock { Text = json }]
        };
    }
}
```

`context` has the action name, the entity key, and the Started JSON. Return `null` to keep that Started payload.

Without a waiter, the harness still shows the URL, then the call ends. The operator finishing in the browser never comes back on MCP.

If the harness cannot show a URL, they get the Started ticket and the human says when they are done.

> **If you also enabled Tasks**
>
> The same elicitation is nested on the task. The operator accepts or declines; accept runs the waiter; decline skips it. Pair `WithTasks` and `WaitForAnnotatedAction` in the same `AddODataMcp` callback.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.