Skip to content

Latest commit

History

History

README.md

Copilot Python SDK

Python SDK for programmatic control of GitHub Copilot CLI via JSON-RPC.

Installation

pip install github-copilot-sdk

To include OpenTelemetry support:

pip install "github-copilot-sdk[telemetry]"

Run the Sample

Try the interactive chat sample (from the repo root):

cd python/samples
python chat.py

Quick Start

importasynciofromcopilotimportCopilotClientfromcopilot.generated.session_eventsimportAssistantMessageData, SessionIdleDatafromcopilot.sessionimportPermissionHandlerasyncdefmain():
# Client automatically starts on enter and cleans up on exitasyncwithCopilotClient() asclient:
# Create a session with automatic cleanupasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) assession:
# Wait for response using session.idle eventdone=asyncio.Event()
defon_event(event):
matchevent.data:
caseAssistantMessageData() asdata:
print(data.content)
caseSessionIdleData():
done.set()
session.on(on_event)
# Send a message and wait for completionawaitsession.send("What is 2+2?")
awaitdone.wait()
asyncio.run(main())

Manual Resource Management

If you need more control over the lifecycle, you can call start(), stop(), and disconnect() manually:

importasynciofromcopilotimportCopilotClientfromcopilot.generated.session_eventsimportAssistantMessageData, SessionIdleDatafromcopilot.sessionimportPermissionHandlerasyncdefmain():
client=CopilotClient()
awaitclient.start()
# Create a session (on_permission_request is optional; approve_all allows every tool)session=awaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
)
done=asyncio.Event()
defon_event(event):
matchevent.data:
caseAssistantMessageData() asdata:
print(data.content)
caseSessionIdleData():
done.set()
session.on(on_event)
awaitsession.send("What is 2+2?")
awaitdone.wait()
# Clean up manuallyawaitsession.disconnect()
awaitclient.stop()
asyncio.run(main())

Features

  • ✅ Full JSON-RPC protocol support
  • ✅ stdio and TCP transports
  • ✅ Real-time streaming events
  • ✅ Session history with get_events()
  • ✅ Type hints throughout
  • ✅ Async/await native
  • ✅ Async context manager support for automatic resource cleanup

API Reference

CopilotClient

fromcopilotimportCopilotClientfromcopilot.sessionimportPermissionHandlerasyncwithCopilotClient() asclient:
asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) assession:
defon_event(event):
print(f"Event: {event.type}")
session.on(on_event)
awaitsession.send("Hello!")
# ... wait for events ...

Note: For manual lifecycle management, see Manual Resource Management above.

fromcopilotimportCopilotClient, RuntimeConnection# Connect to an existing CLI serverclient=CopilotClient(connection=RuntimeConnection.for_uri("localhost:3000"))

CopilotClient Constructor:

CopilotClient() # spawn the bundled runtime with defaultsCopilotClient(connection=..., log_level="debug", github_token=..., ...)

All options are kw-only parameters:

  • connection (RuntimeConnection | None): How to reach the runtime. Use RuntimeConnection.for_stdio(...), RuntimeConnection.for_tcp(...), or RuntimeConnection.for_uri(...). Defaults to a stdio connection with the bundled binary.
  • working_directory (str | None): Working directory for the CLI process (default: current dir).
  • log_level (str): Log level (default: "info").
  • env (dict | None): Environment variables for the CLI process.
  • github_token (str | None): GitHub token for authentication. When provided, takes priority over other auth methods.
  • base_directory (str | None): Base directory for Copilot data (session state, config, etc.). Sets COPILOT_HOME on the spawned CLI process. When None, the CLI defaults to ~/.copilot. Useful in restricted environments where only specific directories are writable. Ignored when using a UriRuntimeConnection.
  • use_logged_in_user (bool | None): Whether to use logged-in user for authentication (default: True, but False when github_token is provided).
  • telemetry (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See Telemetry below.
  • session_fs (dict | None): Connection-level session filesystem provider configuration.
  • session_idle_timeout_seconds (int | None): Server-wide session idle timeout in seconds. Set to None or 0 to disable.
  • enable_remote_sessions (bool): Enable remote/cloud session support (default: False).
  • on_list_models (callable | None): Custom handler for list_models(). When provided, the handler is called instead of querying the runtime.
  • mode (str): Client mode (default: "copilot-cli").

RuntimeConnection variants:

  • RuntimeConnection.for_stdio(path=None, args=None) — spawn a local CLI process and talk over stdio.
  • RuntimeConnection.for_tcp(port=0, connection_token=None, path=None, args=None) — spawn a local CLI in TCP mode.
  • RuntimeConnection.for_uri(url, connection_token=None) — connect to an existing CLI server (e.g. "localhost:8080").

CopilotClient.create_session():

These are passed as keyword arguments to create_session():

  • model (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). Required when using custom provider.
  • reasoning_effort (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use list_models() to check which models support this option.
  • session_id (str): Custom session ID
  • tools (list): Custom tools exposed to the CLI. Tools with handler=None are declaration-only and must be resolved via pending tool-call RPCs.
  • system_message (SystemMessageConfig): System message configuration
  • streaming (bool): Enable streaming delta events
  • provider (ProviderConfig): Custom API provider configuration (BYOK). See Custom Providers section.
  • infinite_sessions (InfiniteSessionConfig): Automatic context compaction configuration
  • on_permission_request (callable): Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. Use PermissionHandler.approve_all to allow everything, or provide a custom function for fine-grained control. See Permission Handling section.
  • on_user_input_request (callable): Handler for user input requests from the agent (enables ask_user tool). See User Input Requests section.
  • hooks (SessionHooks): Hook handlers for session lifecycle events. See Session Hooks section.

Session Lifecycle Methods:

# Get the session currently displayed in TUI (TUI+server mode only)session_id=awaitclient.get_foreground_session_id()
# Request TUI to display a specific session (TUI+server mode only)awaitclient.set_foreground_session_id("session-123")
# Subscribe to all lifecycle eventsdefon_lifecycle(event):
print(f"{event.type}: {event.session_id}")
unsubscribe=client.on_lifecycle(on_lifecycle)
# Subscribe to specific event typeunsubscribe=client.on_lifecycle("session.foreground", lambdae: print(f"Foreground: {e.session_id}"))
# Later, to stop receiving events:unsubscribe()

Lifecycle Event Types:

  • session.created - A new session was created
  • session.deleted - A session was deleted
  • session.updated - A session was updated
  • session.foreground - A session became the foreground session in TUI
  • session.background - A session is no longer the foreground session

Tools

Define tools with automatic JSON schema generation using the @define_tool decorator and Pydantic models:

frompydanticimportBaseModel, FieldfromcopilotimportCopilotClient, define_toolclassLookupIssueParams(BaseModel):
id: str=Field(description="Issue identifier")
@define_tool(description="Fetch issue details from our tracker")asyncdeflookup_issue(params: LookupIssueParams) ->str:
issue=awaitfetch_issue(params.id)
returnissue.summaryasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
tools=[lookup_issue],
) assession:
...

Note: When using from __future__ import annotations, define Pydantic models at module level (not inside functions).

Low-level API (without Pydantic):

For users who prefer manual schema definition:

fromcopilotimportCopilotClientfromcopilot.toolsimportTool, ToolInvocation, ToolResultfromcopilot.sessionimportPermissionHandlerasyncdeflookup_issue(invocation: ToolInvocation) ->ToolResult:
issue_id=invocation.arguments["id"]
issue=awaitfetch_issue(issue_id)
returnToolResult(
text_result_for_llm=issue.summary,
result_type="success",
session_log=f"Fetched issue {issue_id}",
)
asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
tools=[
Tool(
name="lookup_issue",
description="Fetch issue details from our tracker",
parameters={
"type": "object",
"properties": {
"id": {"type": "string", "description": "Issue identifier"},
},
"required": ["id"],
},
handler=lookup_issue,
)
],
) assession:
...

The SDK automatically handles tool.call, executes your handler (sync or async), and responds with the final result when the tool completes. If a tool has no handler, it is exposed as a declaration only; observe external_tool.requested events and resolve the call with the pending tool RPC.

You can also create a declaration-only tool with generated Pydantic parameters:

tool=define_tool(
"lookup_issue",
description="Fetch issue details from our tracker",
params_type=LookupIssueParams,
)

Overriding Built-in Tools

If you register a tool with the same name as a built-in CLI tool (e.g. edit_file, read_file), the SDK will throw an error unless you explicitly opt in by setting overrides_built_in_tool=True. This flag signals that you intend to replace the built-in tool with your custom implementation.

classEditFileParams(BaseModel):
path: str=Field(description="File path")
content: str=Field(description="New file content")
@define_tool(name="edit_file", description="Custom file editor with project-specific validation", overrides_built_in_tool=True)asyncdefedit_file(params: EditFileParams) ->str:
# your logic

Skipping Permission Prompts

Set skip_permission=True on a tool definition to allow it to execute without triggering a permission prompt:

@define_tool(name="safe_lookup", description="A read-only lookup that needs no confirmation", skip_permission=True)asyncdefsafe_lookup(params: LookupParams) ->str:
# your logic

Image Support

The SDK supports image attachments via the attachments parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:

# File attachment — runtime reads from diskawaitsession.send(
"What's in this image?",
attachments=[
{
"type": "file",
"path": "/path/to/image.jpg",
}
],
)
# Blob attachment — provide base64 data directlyawaitsession.send(
"What's in this image?",
attachments=[
{
"type": "blob",
"data": base64_image_data,
"mimeType": "image/png",
}
],
)

Supported image formats include JPG, PNG, GIF, and other common image types. The agent's view tool can also read images directly from the filesystem, so you can also ask questions like:

awaitsession.send("What does the most recent jpg in this directory portray?")

Streaming

Enable streaming to receive assistant response chunks as they're generated:

importasynciofromcopilotimportCopilotClientfromcopilot.generated.session_eventsimport (
AssistantMessageData,
AssistantMessageDeltaData,
AssistantReasoningData,
AssistantReasoningDeltaData,
SessionIdleData,
)
fromcopilot.sessionimportPermissionHandlerasyncdefmain():
asyncwithCopilotClient() asclient:
asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
streaming=True,
) assession:
# Use asyncio.Event to wait for completiondone=asyncio.Event()
defon_event(event):
matchevent.data:
caseAssistantMessageDeltaData() asdata:
# Streaming message chunk - print incrementallydelta=data.delta_contentor""print(delta, end="", flush=True)
caseAssistantReasoningDeltaData() asdata:
# Streaming reasoning chunk (if model supports reasoning)delta=data.delta_contentor""print(delta, end="", flush=True)
caseAssistantMessageData() asdata:
# Final message - complete contentprint("\n--- Final message ---")
print(data.content)
caseAssistantReasoningData() asdata:
# Final reasoning content (if model supports reasoning)print("--- Reasoning ---")
print(data.content)
caseSessionIdleData():
# Session finished processingdone.set()
session.on(on_event)
awaitsession.send("Tell me a short story")
awaitdone.wait() # Wait for streaming to completeasyncio.run(main())

When streaming=True:

  • assistant.message_delta events are sent with delta_content containing incremental text
  • assistant.reasoning_delta events are sent with delta_content for reasoning/chain-of-thought (model-dependent)
  • Accumulate delta_content values to build the full response progressively
  • The final assistant.message and assistant.reasoning events contain the complete content

Note: assistant.message and assistant.reasoning (final events) are always sent regardless of streaming setting.

Infinite Sessions

By default, sessions use infinite sessions which automatically manage context window limits through background compaction and persist state to a workspace directory.

# Default: infinite sessions enabled with default thresholdsasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
) assession:
# Access the workspace path for checkpoints and filesprint(session.workspace_path)
# => ~/.copilot/session-state/{session_id}/# Custom thresholdsasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
infinite_sessions={
"enabled": True,
"background_compaction_threshold": 0.80, # Start compacting at 80% context usage"buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes
},
) assession:
...
# Disable infinite sessionsasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
infinite_sessions={"enabled": False},
) assession:
...

When enabled, sessions emit compaction events:

  • session.compaction_start - Background compaction started
  • session.compaction_complete - Compaction finished (includes token counts)

Custom Providers

The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own Key), including local providers like Ollama. When using a custom provider, you must specify the model explicitly.

ProviderConfig fields:

  • type (str): Provider type - "openai", "azure", or "anthropic" (default: "openai")
  • base_url (str): API endpoint URL (required)
  • api_key (str): API key (optional for local providers like Ollama)
  • bearer_token (str): Bearer token for authentication (takes precedence over api_key)
  • wire_api (str): API format for OpenAI/Azure - "completions" or "responses" (default: "completions")
  • azure (dict): Azure-specific options with api_version (default: "2024-10-21")

Example with Ollama:

asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="deepseek-coder-v2:16b", # Required when using custom providerprovider={
"type": "openai",
"base_url": "http://localhost:11434/v1", # Ollama endpoint# api_key not required for Ollama
},
) assession:
awaitsession.send("Hello!")

Example with custom OpenAI-compatible API:

importosasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-4",
provider={
"type": "openai",
"base_url": "https://my-api.example.com/v1",
"api_key": os.environ["MY_API_KEY"],
},
) assession:
...

Example with Azure OpenAI:

importosasyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-4",
provider={
"type": "azure", # Must be "azure" for Azure endpoints, NOT "openai""base_url": "https://my-resource.openai.azure.com", # Just the host, no path"api_key": os.environ["AZURE_OPENAI_KEY"],
"azure": {
"api_version": "2024-10-21",
},
},
) assession:
...

Important notes:

  • When using a custom provider, the model parameter is required. The SDK will throw an error if no model is specified.
  • For Azure OpenAI endpoints (*.openai.azure.com), you must use type: "azure", not type: "openai".
  • The base_url should be just the host (e.g., https://my-resource.openai.azure.com). Do not include /openai/v1 in the URL - the SDK handles path construction automatically.

Telemetry

The SDK supports OpenTelemetry for distributed tracing. Provide a telemetry config to enable trace export and automatic W3C Trace Context propagation.

fromcopilotimportCopilotClientclient=CopilotClient(
telemetry={
"otlp_endpoint": "http://localhost:4318",
},
)

TelemetryConfig options:

  • otlp_endpoint (str): OTLP HTTP endpoint URL
  • file_path (str): File path for JSON-lines trace output
  • exporter_type (str): "otlp-http" or "file"
  • source_name (str): Instrumentation scope name
  • capture_content (bool): Whether to capture message content

Trace context (traceparent/tracestate) is automatically propagated between the SDK and CLI on create_session, resume_session, and send calls, and inbound when the CLI invokes tool handlers.

Install with telemetry extras: pip install "github-copilot-sdk[telemetry]" (provides opentelemetry-api)

Permission Handling

An on_permission_request handler is optional when you create or resume a session. When provided, it is called before the agent executes each tool (file writes, shell commands, custom tools, etc.) and returns a decision. When omitted, permission requests are emitted as events and left pending for the consumer to resolve with the pending permission RPC.

Approve All (simplest)

Use the built-in PermissionHandler.approve_all helper to allow every tool call without any checks:

fromcopilotimportCopilotClientfromcopilot.sessionimportPermissionHandlersession=awaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
)

Custom Permission Handler

Provide your own function to inspect each request and apply custom logic (sync or async):

fromcopilotimportPermissionRequest, PermissionRequestResultfromcopilot.generated.rpcimport (
PermissionDecisionApproveOnce,
PermissionDecisionReject,
)
fromcopilot.generated.session_eventsimportPermissionRequestShelldefon_permission_request(
request: PermissionRequest, invocation: dict
) ->PermissionRequestResult:
# ``PermissionRequest`` is a discriminated union — pattern-match on# the variant class to access the per-kind fields.matchrequest:
casePermissionRequestShell(full_command_text=cmd):
# Deny shell commandsreturnPermissionDecisionReject(feedback=f"Shell denied: {cmd}")
case _:
returnPermissionDecisionApproveOnce()
session=awaitclient.create_session(
on_permission_request=on_permission_request,
model="gpt-5",
)

Async handlers are also supported:

asyncdefon_permission_request(
request: PermissionRequest, invocation: dict
) ->PermissionRequestResult:
# Simulate an async approval check (e.g., prompting a user over a network)awaitasyncio.sleep(0)
returnPermissionDecisionApproveOnce()

Permission Result Kinds

The handler returns a PermissionRequestResult, which is an alias for PermissionDecision | PermissionNoResult (the generated wire-level union of every decision variant, plus a small sentinel for v1 servers). Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on permission.completed session events.

VariantMeaning
PermissionDecisionApproveOnce()Allow this single request
PermissionDecisionReject(feedback="…")Deny the request (optional feedback string forwarded to the LLM)
PermissionDecisionUserNotAvailable()Deny the request because no user is available to confirm it (the default)
PermissionNoResult()Leave the request unanswered (only valid with protocol v1; rejected by protocol v2 servers)

Several richer variants (PermissionDecisionApproveForSession, PermissionDecisionApproveForLocation, PermissionDecisionApprovePermanently, …) are available for granting longer-lived approvals; see the generated copilot.generated.rpc module for the full list.

Resuming Sessions

You may pass on_permission_request when resuming a session too:

session=awaitclient.resume_session(
"session-id",
on_permission_request=PermissionHandler.approve_all,
)

Per-Tool Skip Permission

To let a specific custom tool bypass the permission prompt entirely, set skip_permission=True on the tool definition. See Skipping Permission Prompts under Tools.

User Input Requests

Enable the agent to ask questions to the user using the ask_user tool by providing an on_user_input_request handler:

asyncdefhandle_user_input(request, invocation):
# request["question"] - The question to ask# request.get("choices") - Optional list of choices for multiple choice# request.get("allowFreeform", True) - Whether freeform input is allowedprint(f"Agent asks: {request['question']}")
ifrequest.get("choices"):
print(f"Choices: {', '.join(request['choices'])}")
# Return the user's responsereturn {
"answer": "User's answer here",
"wasFreeform": True, # Whether the answer was freeform (not from choices)
}
asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
on_user_input_request=handle_user_input,
) assession:
...

Session Hooks

Hook into session lifecycle events by providing handlers in the hooks configuration:

asyncdefon_pre_tool_use(input, invocation):
print(f"About to run tool: {input['toolName']}")
# Return permission decision and optionally modify argsreturn {
"permissionDecision": "allow", # "allow", "deny", or "ask""modifiedArgs": input.get("toolArgs"), # Optionally modify tool arguments"additionalContext": "Extra context for the model",
}
asyncdefon_post_tool_use(input, invocation):
print(f"Tool {input['toolName']} completed")
return {
"additionalContext": "Post-execution notes",
}
asyncdefon_post_tool_use_failure(input, invocation):
# Fires when a tool's result was a failure. `on_post_tool_use` only fires# on success, so register this handler to observe failed tool calls. The# CLI extracts the failure message and passes it as the `error` field.print(f"Tool {input['toolName']} failed: {input['error']}")
return {
"additionalContext": f"Retry guidance for {input['toolName']}",
}
asyncdefon_user_prompt_submitted(input, invocation):
print(f"User prompt: {input['prompt']}")
return {
"modifiedPrompt": input["prompt"], # Optionally modify the prompt
}
asyncdefon_session_start(input, invocation):
print(f"Session started from: {input['source']}") # "startup", "resume", "new"return {
"additionalContext": "Session initialization context",
}
asyncdefon_session_end(input, invocation):
print(f"Session ended: {input['reason']}")
asyncdefon_error_occurred(input, invocation):
print(f"Error in {input['errorContext']}: {input['error']}")
return {
"errorHandling": "retry", # "retry", "skip", or "abort"
}
asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
model="gpt-5",
hooks={
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
"on_post_tool_use_failure": on_post_tool_use_failure,
"on_user_prompt_submitted": on_user_prompt_submitted,
"on_session_start": on_session_start,
"on_session_end": on_session_end,
"on_error_occurred": on_error_occurred,
},
) assession:
...

Available hooks:

  • on_pre_tool_use - Intercept tool calls before execution. Can allow/deny or modify arguments.
  • on_post_tool_use - Process tool results after successful execution. Can modify results or add context.
  • on_post_tool_use_failure - Observe failed tool executions and inject extra context to guide the model's next step.
  • on_user_prompt_submitted - Intercept user prompts. Can modify the prompt before processing.
  • on_session_start - Run logic when a session starts or resumes.
  • on_session_end - Cleanup or logging when session ends.
  • on_error_occurred - Handle errors with retry/skip/abort strategies.

Commands

Register slash commands that users can invoke from the CLI TUI. When the user types /commandName, the SDK dispatches the event to your handler.

fromcopilot.sessionimportCommandDefinition, CommandContext, PermissionHandlerasyncdefhandle_deploy(ctx: CommandContext) ->None:
print(f"Deploying with args: {ctx.args}")
# ctx.session_id — the session where the command was invoked# ctx.command — full command text (e.g. "/deploy production")# ctx.command_name — command name without leading / (e.g. "deploy")# ctx.args — raw argument string (e.g. "production")asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
commands=[
CommandDefinition(
name="deploy",
description="Deploy the app",
handler=handle_deploy,
),
CommandDefinition(
name="rollback",
description="Rollback to previous version",
handler=lambdactx: print("Rolling back..."),
),
],
) assession:
...

Commands can also be provided when resuming a session via resume_session(commands=[...]).

UI Elicitation

The session.ui API provides convenience methods for asking the user questions through interactive dialogs. These methods are only available when the CLI host supports elicitation — check session.capabilities before calling.

Capability Check

ui_caps=session.capabilities.get("ui", {})
ifui_caps.get("elicitation"):
# Safe to call session.ui methods
...

Confirm

Shows a yes/no confirmation dialog:

ok=awaitsession.ui.confirm("Deploy to production?")
ifok:
print("Deploying...")

Select

Shows a selection dialog with a list of options:

env=awaitsession.ui.select("Choose environment:", ["staging", "production", "dev"])
ifenv:
print(f"Selected: {env}")

Input

Shows a text input dialog with optional constraints:

name=awaitsession.ui.input("Enter your name:")
# With optionsemail=awaitsession.ui.input("Enter email:", {
"title": "Email Address",
"description": "We'll use this for notifications",
"format": "email",
})

Custom Elicitation

For full control, use the elicitation() method with a custom JSON schema:

result=awaitsession.ui.elicitation({
"message": "Configure deployment",
"requestedSchema": {
"type": "object",
"properties": {
"region": {"type": "string", "enum": ["us-east-1", "eu-west-1"]},
"replicas": {"type": "number", "minimum": 1, "maximum": 10},
},
"required": ["region"],
},
})
ifresult["action"] =="accept":
region=result["content"]["region"]
replicas=result["content"].get("replicas", 1)

Elicitation Request Handler

When the server (or an MCP tool) needs to ask the end-user a question, it sends an elicitation.requested event. Provide an on_elicitation_request handler to respond:

fromcopilot.sessionimportElicitationContext, ElicitationResult, PermissionHandlerasyncdefhandle_elicitation(
context: ElicitationContext,
) ->ElicitationResult:
# context["session_id"] — the session ID# context["message"] — what the server is asking# context.get("requestedSchema") — optional JSON schema for form fields# context.get("mode") — "form" or "url"print(f"Server asks: {context['message']}")
# Return the user's responsereturn {
"action": "accept", # or "decline" or "cancel""content": {"answer": "yes"},
}
asyncwithawaitclient.create_session(
on_permission_request=PermissionHandler.approve_all,
on_elicitation_request=handle_elicitation,
) assession:
...

When on_elicitation_request is provided, the SDK automatically:

  • Sends requestElicitation: true to the server during session creation/resumption
  • Reports the elicitation capability on the session
  • Dispatches elicitation.requested events to your handler
  • Auto-cancels if your handler throws an error (so the server doesn't hang)

Requirements

  • Python 3.11+
  • GitHub Copilot CLI installed and accessible