Time: 10 minutes
You'll produce a concise report that separates page evidence, criterion mapping, remediation, and the limits of the review.
An agent response contains evidence and interpretation. Evidence is what Playwright observed, such as an input with no accessible name. Interpretation is the criterion mapping and remediation based on that evidence and the catalog result.
A clear output contract tells the agent what to include, what to leave out, and how to handle uncertainty. It makes reports more consistent without claiming that the review is exhaustive.
One automated snapshot cannot establish accessibility conformance. The report should stick to high-confidence findings without invented statistics, decorative severity labels, or a broad claim that the page passes or fails WCAG.
The agent now turns browser evidence + catalog result into a bounded, repeatable report.
:::language dotnet
Create Helpers/Prompts.cs:
namespace HelloCopilotSDK.Helpers;
public static class Prompts
{
public static string CreateReportPrompt(Uri targetUri) => $"""
Prepare an evidence-based accessibility review of {targetUri.AbsoluteUri}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant.
""";
}::: :::language dotnet
Replace the final send call in Program.cs:
Console.WriteLine($"\nAnalyzing: {targetUri.AbsoluteUri}\n");
await ResponseStreamer.SendAndPrintAsync(session, Prompts.CreateReportPrompt(targetUri));:::
:::language nodejs
In src/workshop.ts, add or replace reportPrompt:
export function reportPrompt(target: URL): string {
return `Prepare an evidence-based accessibility review of ${target.href}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant.`;
}::: :::language nodejs
Create or replace src/report.ts with the URL parsing, session config, and prompt:
import { CopilotClient } from "@github/copilot-sdk";
import { accessibilityRuleLookup, createSnapshotReader, permissionForTarget, reportPrompt, streamResponse } from "./workshop.js";
const input = process.argv[2];
if (!input) throw new Error("Usage: npm start -- <http-or-https-url>");
const target = new URL(input.includes("://") ? input : `https://${input}`);
if (!["http:", "https:"].includes(target.protocol)) throw new Error("Enter an absolute HTTP or HTTPS URL.");
const client = new CopilotClient();
await client.start();
try {
const session = await client.createSession({
streaming: true, onPermissionRequest: permissionForTarget(target),
tools: [accessibilityRuleLookup, createSnapshotReader(process.cwd())],
availableTools: ["accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"],
mcpServers: { playwright: { command: "npx", args: ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"], workingDirectory: process.cwd(), tools: ["browser_navigate"] } },
});
try { await streamResponse(session, reportPrompt(target)); } finally { await session.disconnect(); }
} finally { await client.stop(); }::: :::language nodejs
Replace src/index.ts so the package start command launches the report entrypoint:
import "./report.js";:::
:::language python
In workshop.py, add or replace report_prompt:
def report_prompt(target: str) -> str:
return f"""Prepare an evidence-based accessibility review of {target}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant."""::: :::language python
Create or replace report.py with URL parsing, session config, streaming, and the report prompt:
import asyncio
import sys
from urllib.parse import urlsplit
from copilot import CopilotClient
from copilot.session_events import AssistantMessageData, AssistantMessageDeltaData, SessionErrorData, SessionIdleData, ToolExecutionCompleteData, ToolExecutionStartData
from workshop import accessibility_rule_lookup, create_snapshot_reader, permission_for_target, report_prompt
async def main() -> None:
target = sys.argv[1] if len(sys.argv) == 2 else input("Enter URL to analyze: ").strip()
target = target if "://" in target else f"https://{target}"
if urlsplit(target).scheme not in {"http", "https"}:
raise ValueError("Enter an absolute HTTP or HTTPS URL.")
async with CopilotClient() as client:
async with await client.create_session(streaming=True, on_permission_request=permission_for_target(target), tools=[accessibility_rule_lookup, create_snapshot_reader(".")], available_tools=["accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"], mcp_servers={"playwright": {"command": "npx", "args": ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"], "working_directory": ".", "tools": ["browser_navigate"]}}) as session:
done = asyncio.Event()
error: RuntimeError | None = None
received_delta = False
def on_event(event) -> None:
nonlocal error, received_delta
match event.data:
case AssistantMessageDeltaData(delta_content=delta) if delta:
received_delta = True
print(delta, end="", flush=True)
case AssistantMessageData(content=content) if content and not received_delta:
print(content)
case ToolExecutionStartData(tool_name=name): print(f"\n[tool:start] {name}")
case ToolExecutionCompleteData(success=success): print(f"[tool:done] success={success}")
case SessionErrorData(message=message):
error = RuntimeError(message)
done.set()
case SessionIdleData(): done.set()
session.on(on_event)
await session.send(report_prompt(target))
await done.wait()
if error is not None:
raise error
if __name__ == "__main__":
asyncio.run(main())::: :::language python
Replace main.py so the documented command launches the report entrypoint:
from report import main
import asyncio
if __name__ == "__main__":
asyncio.run(main()):::
:::language go
In main.go, add reportPrompt:
func reportPrompt(target string) string {
return fmt.Sprintf(`Prepare an evidence-based accessibility review of %s.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot for browser-observable evidence.
3. Call accessibility_rule_lookup before each recommendation.
Return only:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific browser evidence>
- WCAG criterion: <catalog result>
- Recommended remediation: <specific change>
## Review limits
State that this focused review is not a full WCAG conformance audit.`, target)
}::: :::language go
Replace main so it validates the URL argument, builds the three-tool session, and sends the report
prompt:
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "Usage: go run . <http-or-https-url>")
return
}
target := os.Args[1]
if !strings.Contains(target, "://") {
target = "https://" + target
}
parsed, err := url.ParseRequestURI(target)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
fmt.Fprintln(os.Stderr, "Enter an absolute HTTP or HTTPS URL.")
return
}
workingDirectory, err := os.Getwd()
if err != nil {
panic(err)
}
lookup := copilot.DefineTool("accessibility_rule_lookup", "Looks up read-only WCAG guidance maintained by this application.", accessibilityRuleLookup)
lookup.SkipPermission = true
readSnapshot := copilot.DefineTool("read_latest_accessibility_snapshot", "Reads the newest Playwright accessibility snapshot created during this run.", snapshotReader(workingDirectory))
readSnapshot.SkipPermission = true
client := copilot.NewClient(&copilot.ClientOptions{LogLevel: "error"})
if err := client.Start(context.Background()); err != nil {
panic(err)
}
defer client.Stop()
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Streaming: copilot.Bool(true),
Tools: []copilot.Tool{lookup, readSnapshot},
AvailableTools: []string{"accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"},
OnPermissionRequest: permissionForTarget(target),
MCPServers: map[string]copilot.MCPServerConfig{
"playwright": copilot.MCPStdioServerConfig{
Command: "npx",
Args: []string{"-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"},
WorkingDirectory: workingDirectory,
Tools: []string{"browser_navigate"},
},
},
})
if err != nil {
panic(err)
}
defer session.Disconnect()
if err := streamResponse(session, reportPrompt(target)); err != nil {
panic(err)
}
}:::
:::language rust
In src/main.rs, add report_prompt:
fn report_prompt(target: &Url) -> String {
format!(
r#"Prepare an evidence-based accessibility review of {target}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant."#
)
}::: :::language rust
Replace main so it validates the URL argument, builds the three-tool session, and sends the report
prompt:
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let argument = std::env::args()
.nth(1)
.ok_or("Usage: cargo run -- <http-or-https-url>")?;
let target_text = if argument.contains("://") {
argument
} else {
format!("https://{argument}")
};
let target = Url::parse(&target_text)?;
if !matches!(target.scheme(), "http" | "https") || target.host_str().is_none() {
return Err("Enter an absolute HTTP or HTTPS URL.".into());
}
let working_directory = std::env::current_dir()?;
let lookup = Tool::new("accessibility_rule_lookup")
.with_description("Looks up read-only WCAG guidance maintained by this application.")
.with_parameters(schema_for::<LookupParams>())
.with_skip_permission(true)
.with_handler(Arc::new(AccessibilityRuleLookup));
let reader = Tool::new("read_latest_accessibility_snapshot")
.with_description(
"Reads the newest Playwright accessibility snapshot created during this run.",
)
.with_parameters(
serde_json::json!({"type": "object", "properties": {}, "additionalProperties": false}),
)
.with_skip_permission(true)
.with_handler(Arc::new(SnapshotReader::new(&working_directory)));
let mut config = SessionConfig::default();
config.streaming = Some(true);
config.tools = Some(vec![lookup, reader]);
config.available_tools = Some(vec![
"accessibility_rule_lookup".to_owned(),
"read_latest_accessibility_snapshot".to_owned(),
"playwright-browser_navigate".to_owned(),
]);
config.mcp_servers = Some(IndexMap::from([(
"playwright".to_owned(),
McpServerConfig::Stdio(McpStdioServerConfig {
command: "npx".to_owned(),
args: vec![
"-y".to_owned(),
"@playwright/mcp@0.0.78".to_owned(),
"--browser=msedge".to_owned(),
"--output-dir".to_owned(),
".playwright-mcp".to_owned(),
"--output-mode".to_owned(),
"file".to_owned(),
],
tools: Some(vec!["browser_navigate".to_owned()]),
working_directory: Some(working_directory.display().to_string()),
..Default::default()
}),
)]));
let config = config.with_permission_handler(Arc::new(ScopedPermissions {
target: target.clone(),
}));
let client = Client::start(ClientOptions::default()).await?;
let session = client.create_session(config).await?;
stream_response!(session, report_prompt(&target));
session.disconnect().await?;
client.stop().await?;
Ok(())
}:::
:::language java
In src/main/java/workshop/AccessibilityReport.java, add reportPrompt:
private static String reportPrompt(URI target) {
return """
Prepare an evidence-based accessibility review of %s.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant.""".formatted(target);
}Keep the Step 4 permission callback unchanged. It remains fail-closed unless you explicitly pass
--allow-local-demo-mcp for the controlled workshop target. That temporary mcp-kind-only fallback
does not prove the exact URL because github/copilot-sdk#2273
does not expose the request payload; do not use it outside a disposable local workshop demo.
:::
:::language java
Replace main so it validates the URL argument, builds the three-tool session, and sends the report
prompt:
private static final String LOCAL_DEMO_MCP_FLAG = "--allow-local-demo-mcp";
public static void main(String[] args) throws Exception {
RunOptions options = parseRunOptions(args);
URI target = options.target();
Path workingDirectory = Path.of("").toAbsolutePath().normalize();
if (options.allowLocalDemoMcp()) {
System.err.println("WARNING: Local demo fallback enabled. MCP request payload fields are unavailable, "
+ "so this run approves only the mcp permission kind, not an exact target. "
+ "Use only with the controlled workshop target.");
}
var lookup = ToolDefinition.from(
"accessibility_rule_lookup",
"Looks up read-only WCAG guidance maintained by this application.",
Param.of(String.class, "query", "The accessibility issue or WCAG criterion to look up."),
AccessibilityReport::lookupRule).skipPermission(true);
var readSnapshot = ToolDefinition.from(
"read_latest_accessibility_snapshot",
"Reads the newest Playwright accessibility snapshot created during this run.",
new SnapshotReader(workingDirectory)::read).skipPermission(true);
var config = new SessionConfig()
.setStreaming(true)
.setTools(List.of(lookup, readSnapshot))
.setAvailableTools(List.of(
"accessibility_rule_lookup",
"read_latest_accessibility_snapshot",
"playwright-browser_navigate"))
.setMcpServers(Map.of("playwright", new McpStdioServerConfig()
.setCommand("npx")
.setArgs(List.of("-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"))
.setWorkingDirectory(workingDirectory.toString())
.setTools(List.of("browser_navigate"))))
.setOnPermissionRequest((request, ignored) -> {
if ("mcp".equals(request.getKind())
&& isExactNavigation(request.getExtensionData(), target)) {
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.approveOnce());
}
if (options.allowLocalDemoMcp() && "mcp".equals(request.getKind())) {
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.approveOnce());
}
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.reject(
"This workshop allows Playwright to navigate only to the exact requested target. "
+ "MCP requests without target data remain denied unless the explicit "
+ LOCAL_DEMO_MCP_FLAG + " local-demo fallback is enabled."));
});
try (var client = new CopilotClient()) {
client.start().get();
var session = client.createSession(config).get();
var response = session.sendAndWait(new MessageOptions().setPrompt(reportPrompt(target))).get();
if (response == null) {
throw new IllegalStateException("Copilot completed without an assistant message.");
}
System.out.println(response.getData().content());
}
}Keep the implementation's parseTarget and fallback helpers next to main:
private static URI parseTarget(String value) throws URISyntaxException {
String candidate = value.contains("://") ? value : "https://" + value;
URI target = new URI(candidate);
if (!target.isAbsolute()
|| target.getHost() == null
|| !("http".equalsIgnoreCase(target.getScheme()) || "https".equalsIgnoreCase(target.getScheme()))) {
throw new IllegalArgumentException("Enter an absolute HTTP or HTTPS URL.");
}
return target;
}
private static RunOptions parseRunOptions(String[] args) throws URISyntaxException {
boolean allowLocalDemoMcp = false;
String target = null;
for (String arg : args) {
if (LOCAL_DEMO_MCP_FLAG.equals(arg)) {
if (allowLocalDemoMcp) {
throw new IllegalArgumentException("Specify " + LOCAL_DEMO_MCP_FLAG + " at most once.");
}
allowLocalDemoMcp = true;
} else if (target == null) {
target = arg;
} else {
throw new IllegalArgumentException(usage());
}
}
if (target == null) {
throw new IllegalArgumentException(usage());
}
return new RunOptions(parseTarget(target), allowLocalDemoMcp);
}
private static String usage() {
return "Usage: ./mvnw compile exec:java -Dexec.args=\"["
+ LOCAL_DEMO_MCP_FLAG + "] <http-or-https-url>\"";
}
private record RunOptions(URI target, boolean allowLocalDemoMcp) {
}:::
:::language dotnet
dotnet runWhen the app asks for a URL, paste:
{{TARGET_APP_URL}}
::: :::language nodejs
npm start -- "{{TARGET_APP_URL}}"::: :::language python
python main.py "{{TARGET_APP_URL}}"::: :::language go
go run . "{{TARGET_APP_URL}}"::: :::language rust
cargo run -- "{{TARGET_APP_URL}}"::: :::language java
./mvnw compile exec:java -Dexec.args="--allow-local-demo-mcp {{TARGET_APP_URL}}":::
The report should follow this shape:
# Accessibility review
## Finding 1: Input has no accessible name
- Evidence: The snapshot contains a textbox with no accessible name.
- WCAG criterion: 4.1.2 Name, Role, Value
- Recommended remediation: Associate a visible label using matching for and id values.
## Review limits
This focused review uses browser-observable evidence and is not a full WCAG conformance audit.
Troubleshooting this run
| Symptom | Fix |
|---|---|
| The output contains unsupported counts | Confirm the prompt says not to report unsupported statistics. |
| A finding has no concrete element or structure | Treat it as ungrounded; keep the evidence requirement in the report contract. |
| The response claims WCAG compliance | Keep the required Review limits section and explicit prohibition. |
| The package still runs an earlier entrypoint | Point the start command at the Step 6 report entrypoint for your language. |
| The URL is rejected | Pass an HTTP or HTTPS URL; a missing scheme is automatically changed to https://. |
You're ready for the final run when: each finding contains specific browser evidence, a catalog criterion, and a remediation, and the report ends with its limits.
In the report, which content is direct evidence and which content is model interpretation?
Check your answer
The element or page structure returned by Playwright is evidence. Choosing the criterion and writing the remediation are interpretations based on that evidence and the catalog result.
:::language dotnet
Complete Step 6 implementation
For comparison, use the
finished/dotnet/accessibility-report
project.
using GitHub.Copilot;
using HelloCopilotSDK.Helpers;
Console.WriteLine("=== Accessibility Report Generator ===\n");
Console.Write("Enter URL to analyze: ");
var urlInput = Console.ReadLine()?.Trim();
if (string.IsNullOrWhiteSpace(urlInput))
{
Console.Error.WriteLine("Enter a URL to analyze.");
return;
}
if (!urlInput.Contains("://", StringComparison.Ordinal))
{
urlInput = $"https://{urlInput}";
}
if (!Uri.TryCreate(urlInput, UriKind.Absolute, out var targetUri) ||
targetUri.Scheme is not ("http" or "https"))
{
Console.Error.WriteLine("Enter an absolute HTTP or HTTPS URL.");
return;
}
await using var client = new CopilotClient();
await client.StartAsync();
var ping = await client.PingAsync("workshop");
Console.WriteLine($"\nConnected to the Copilot runtime: {ping.Message}\n");
var workingDirectory = Directory.GetCurrentDirectory();
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Streaming = true,
OnPermissionRequest = WorkshopPermissionHandler.CreateForTarget(targetUri),
Tools =
[
AccessibilityRuleCatalog.CreateLookupTool(),
PlaywrightSnapshotReader.CreateTool(workingDirectory)
],
AvailableTools =
[
"accessibility_rule_lookup",
"read_latest_accessibility_snapshot",
"playwright-browser_navigate"
],
McpServers = new Dictionary<string, McpServerConfig>
{
["playwright"] = new McpStdioServerConfig
{
Command = "npx",
Args = ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"],
WorkingDirectory = workingDirectory,
Tools = ["browser_navigate"]
}
}
});
Console.WriteLine($"Analyzing: {targetUri.AbsoluteUri}\n");
await ResponseStreamer.SendAndPrintAsync(session, Prompts.CreateReportPrompt(targetUri));:::language nodejs
Complete Step 6 implementation
For comparison, use the
finished/nodejs/accessibility-report
project.
src/index.ts:
import "./report.js";src/report.ts:
import { CopilotClient } from "@github/copilot-sdk";
import { accessibilityRuleLookup, createSnapshotReader, permissionForTarget, reportPrompt, streamResponse } from "./workshop.js";
const input = process.argv[2];
if (!input) throw new Error("Usage: npm start -- <http-or-https-url>");
const target = new URL(input.includes("://") ? input : `https://${input}`);
if (!["http:", "https:"].includes(target.protocol)) throw new Error("Enter an absolute HTTP or HTTPS URL.");
const client = new CopilotClient();
await client.start();
try {
const session = await client.createSession({
streaming: true, onPermissionRequest: permissionForTarget(target),
tools: [accessibilityRuleLookup, createSnapshotReader(process.cwd())],
availableTools: ["accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"],
mcpServers: { playwright: { command: "npx", args: ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"], workingDirectory: process.cwd(), tools: ["browser_navigate"] } },
});
try { await streamResponse(session, reportPrompt(target)); } finally { await session.disconnect(); }
} finally { await client.stop(); }reportPrompt in src/workshop.ts:
export function reportPrompt(target: URL): string {
return `Prepare an evidence-based accessibility review of ${target.href}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant.`;
}:::language python
Complete Step 6 implementation
For comparison, use the
finished/python/accessibility-report
project.
main.py:
from report import main
import asyncio
if __name__ == "__main__":
asyncio.run(main())report.py:
import asyncio
import sys
from urllib.parse import urlsplit
from copilot import CopilotClient
from copilot.session_events import AssistantMessageData, AssistantMessageDeltaData, SessionErrorData, SessionIdleData, ToolExecutionCompleteData, ToolExecutionStartData
from workshop import accessibility_rule_lookup, create_snapshot_reader, permission_for_target, report_prompt
async def main() -> None:
target = sys.argv[1] if len(sys.argv) == 2 else input("Enter URL to analyze: ").strip()
target = target if "://" in target else f"https://{target}"
if urlsplit(target).scheme not in {"http", "https"}:
raise ValueError("Enter an absolute HTTP or HTTPS URL.")
async with CopilotClient() as client:
async with await client.create_session(streaming=True, on_permission_request=permission_for_target(target), tools=[accessibility_rule_lookup, create_snapshot_reader(".")], available_tools=["accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"], mcp_servers={"playwright": {"command": "npx", "args": ["-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"], "working_directory": ".", "tools": ["browser_navigate"]}}) as session:
done = asyncio.Event()
error: RuntimeError | None = None
received_delta = False
def on_event(event) -> None:
nonlocal error, received_delta
match event.data:
case AssistantMessageDeltaData(delta_content=delta) if delta:
received_delta = True
print(delta, end="", flush=True)
case AssistantMessageData(content=content) if content and not received_delta:
print(content)
case ToolExecutionStartData(tool_name=name): print(f"\n[tool:start] {name}")
case ToolExecutionCompleteData(success=success): print(f"[tool:done] success={success}")
case SessionErrorData(message=message):
error = RuntimeError(message)
done.set()
case SessionIdleData(): done.set()
session.on(on_event)
await session.send(report_prompt(target))
await done.wait()
if error is not None:
raise error
if __name__ == "__main__":
asyncio.run(main())report_prompt in workshop.py:
def report_prompt(target: str) -> str:
return f"""Prepare an evidence-based accessibility review of {target}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant.""":::language go
Complete Step 6 implementation
For comparison, use the
finished/go/accessibility-report
project. The report contract and entrypoint:
func reportPrompt(target string) string {
return fmt.Sprintf(`Prepare an evidence-based accessibility review of %s.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot for browser-observable evidence.
3. Call accessibility_rule_lookup before each recommendation.
Return only:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific browser evidence>
- WCAG criterion: <catalog result>
- Recommended remediation: <specific change>
## Review limits
State that this focused review is not a full WCAG conformance audit.`, target)
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "Usage: go run . <http-or-https-url>")
return
}
target := os.Args[1]
if !strings.Contains(target, "://") {
target = "https://" + target
}
parsed, err := url.ParseRequestURI(target)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
fmt.Fprintln(os.Stderr, "Enter an absolute HTTP or HTTPS URL.")
return
}
workingDirectory, err := os.Getwd()
if err != nil {
panic(err)
}
lookup := copilot.DefineTool("accessibility_rule_lookup", "Looks up read-only WCAG guidance maintained by this application.", accessibilityRuleLookup)
lookup.SkipPermission = true
readSnapshot := copilot.DefineTool("read_latest_accessibility_snapshot", "Reads the newest Playwright accessibility snapshot created during this run.", snapshotReader(workingDirectory))
readSnapshot.SkipPermission = true
client := copilot.NewClient(&copilot.ClientOptions{LogLevel: "error"})
if err := client.Start(context.Background()); err != nil {
panic(err)
}
defer client.Stop()
session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{
Streaming: copilot.Bool(true),
Tools: []copilot.Tool{lookup, readSnapshot},
AvailableTools: []string{"accessibility_rule_lookup", "read_latest_accessibility_snapshot", "playwright-browser_navigate"},
OnPermissionRequest: permissionForTarget(target),
MCPServers: map[string]copilot.MCPServerConfig{
"playwright": copilot.MCPStdioServerConfig{
Command: "npx",
Args: []string{"-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"},
WorkingDirectory: workingDirectory,
Tools: []string{"browser_navigate"},
},
},
})
if err != nil {
panic(err)
}
defer session.Disconnect()
if err := streamResponse(session, reportPrompt(target)); err != nil {
panic(err)
}
}:::language rust
Complete Step 6 implementation
For comparison, use the
finished/rust/accessibility-report
project. The report contract and entrypoint:
fn report_prompt(target: &Url) -> String {
format!(
r#"Prepare an evidence-based accessibility review of {target}.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant."#
)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let argument = std::env::args()
.nth(1)
.ok_or("Usage: cargo run -- <http-or-https-url>")?;
let target_text = if argument.contains("://") {
argument
} else {
format!("https://{argument}")
};
let target = Url::parse(&target_text)?;
if !matches!(target.scheme(), "http" | "https") || target.host_str().is_none() {
return Err("Enter an absolute HTTP or HTTPS URL.".into());
}
let working_directory = std::env::current_dir()?;
let lookup = Tool::new("accessibility_rule_lookup")
.with_description("Looks up read-only WCAG guidance maintained by this application.")
.with_parameters(schema_for::<LookupParams>())
.with_skip_permission(true)
.with_handler(Arc::new(AccessibilityRuleLookup));
let reader = Tool::new("read_latest_accessibility_snapshot")
.with_description(
"Reads the newest Playwright accessibility snapshot created during this run.",
)
.with_parameters(
serde_json::json!({"type": "object", "properties": {}, "additionalProperties": false}),
)
.with_skip_permission(true)
.with_handler(Arc::new(SnapshotReader::new(&working_directory)));
let mut config = SessionConfig::default();
config.streaming = Some(true);
config.tools = Some(vec![lookup, reader]);
config.available_tools = Some(vec![
"accessibility_rule_lookup".to_owned(),
"read_latest_accessibility_snapshot".to_owned(),
"playwright-browser_navigate".to_owned(),
]);
config.mcp_servers = Some(IndexMap::from([(
"playwright".to_owned(),
McpServerConfig::Stdio(McpStdioServerConfig {
command: "npx".to_owned(),
args: vec![
"-y".to_owned(),
"@playwright/mcp@0.0.78".to_owned(),
"--browser=msedge".to_owned(),
"--output-dir".to_owned(),
".playwright-mcp".to_owned(),
"--output-mode".to_owned(),
"file".to_owned(),
],
tools: Some(vec!["browser_navigate".to_owned()]),
working_directory: Some(working_directory.display().to_string()),
..Default::default()
}),
)]));
let config = config.with_permission_handler(Arc::new(ScopedPermissions {
target: target.clone(),
}));
let client = Client::start(ClientOptions::default()).await?;
let session = client.create_session(config).await?;
stream_response!(session, report_prompt(&target));
session.disconnect().await?;
client.stop().await?;
Ok(())
}:::language java
Complete Step 6 implementation
For comparison, use the
finished/java/accessibility-report
project. The report contract, argument parsing, and entrypoint:
private static final String LOCAL_DEMO_MCP_FLAG = "--allow-local-demo-mcp";
public static void main(String[] args) throws Exception {
RunOptions options = parseRunOptions(args);
URI target = options.target();
Path workingDirectory = Path.of("").toAbsolutePath().normalize();
if (options.allowLocalDemoMcp()) {
System.err.println("WARNING: Local demo fallback enabled. MCP request payload fields are unavailable, "
+ "so this run approves only the mcp permission kind, not an exact target. "
+ "Use only with the controlled workshop target.");
}
var lookup = ToolDefinition.from(
"accessibility_rule_lookup",
"Looks up read-only WCAG guidance maintained by this application.",
Param.of(String.class, "query", "The accessibility issue or WCAG criterion to look up."),
AccessibilityReport::lookupRule).skipPermission(true);
var readSnapshot = ToolDefinition.from(
"read_latest_accessibility_snapshot",
"Reads the newest Playwright accessibility snapshot created during this run.",
new SnapshotReader(workingDirectory)::read).skipPermission(true);
var config = new SessionConfig()
.setStreaming(true)
.setTools(List.of(lookup, readSnapshot))
.setAvailableTools(List.of(
"accessibility_rule_lookup",
"read_latest_accessibility_snapshot",
"playwright-browser_navigate"))
.setMcpServers(Map.of("playwright", new McpStdioServerConfig()
.setCommand("npx")
.setArgs(List.of("-y", "@playwright/mcp@0.0.78", "--browser=msedge", "--output-dir", ".playwright-mcp", "--output-mode", "file"))
.setWorkingDirectory(workingDirectory.toString())
.setTools(List.of("browser_navigate"))))
.setOnPermissionRequest((request, ignored) -> {
if ("mcp".equals(request.getKind())
&& isExactNavigation(request.getExtensionData(), target)) {
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.approveOnce());
}
if (options.allowLocalDemoMcp() && "mcp".equals(request.getKind())) {
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.approveOnce());
}
return java.util.concurrent.CompletableFuture.completedFuture(
PermissionRequestResult.reject(
"This workshop allows Playwright to navigate only to the exact requested target. "
+ "MCP requests without target data remain denied unless the explicit "
+ LOCAL_DEMO_MCP_FLAG + " local-demo fallback is enabled."));
});
try (var client = new CopilotClient()) {
client.start().get();
var session = client.createSession(config).get();
var response = session.sendAndWait(new MessageOptions().setPrompt(reportPrompt(target))).get();
if (response == null) {
throw new IllegalStateException("Copilot completed without an assistant message.");
}
System.out.println(response.getData().content());
}
}
private static URI parseTarget(String value) throws URISyntaxException {
String candidate = value.contains("://") ? value : "https://" + value;
URI target = new URI(candidate);
if (!target.isAbsolute()
|| target.getHost() == null
|| !("http".equalsIgnoreCase(target.getScheme()) || "https".equalsIgnoreCase(target.getScheme()))) {
throw new IllegalArgumentException("Enter an absolute HTTP or HTTPS URL.");
}
return target;
}
private static RunOptions parseRunOptions(String[] args) throws URISyntaxException {
boolean allowLocalDemoMcp = false;
String target = null;
for (String arg : args) {
if (LOCAL_DEMO_MCP_FLAG.equals(arg)) {
if (allowLocalDemoMcp) {
throw new IllegalArgumentException("Specify " + LOCAL_DEMO_MCP_FLAG + " at most once.");
}
allowLocalDemoMcp = true;
} else if (target == null) {
target = arg;
} else {
throw new IllegalArgumentException(usage());
}
}
if (target == null) {
throw new IllegalArgumentException(usage());
}
return new RunOptions(parseTarget(target), allowLocalDemoMcp);
}
private static String usage() {
return "Usage: ./mvnw compile exec:java -Dexec.args=\"["
+ LOCAL_DEMO_MCP_FLAG + "] <http-or-https-url>\"";
}
private record RunOptions(URI target, boolean allowLocalDemoMcp) {
}
private static String reportPrompt(URI target) {
return """
Prepare an evidence-based accessibility review of %s.
1. Use browser_navigate to open that exact URL.
2. Call read_latest_accessibility_snapshot to inspect its accessibility tree.
3. Identify three to five high-confidence issues supported by the snapshot.
4. Call accessibility_rule_lookup for each issue before recommending a fix.
Return only this structure:
# Accessibility review
## Finding 1: <short name>
- Evidence: <specific element or page structure observed in the browser>
- WCAG criterion: <criterion and title returned by the catalog>
- Recommended remediation: <specific implementation change>
Repeat the finding section as needed.
## Review limits
State that this is a focused review of browser-observable evidence, not a full WCAG conformance audit.
Do not invent evidence, report unsupported statistics, or claim the page is WCAG compliant.""".formatted(target);
}- User prompt submitted hook: modifying or rejecting a prompt in code before the runtime sends it.
- User prompt transformed hook: inspecting the model-facing prompt the runtime actually built from your text.
- Citations: an experimental way to tie spans of a response back to the material that supports them.
Continue to Step 7: Run and explain the application.