一键导入
async-credential-prompting
How to implement async credential prompting with proper error handling across multiple implementations (console, MCP elicitation, etc.)
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
How to implement async credential prompting with proper error handling across multiple implementations (console, MCP elicitation, etc.)
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | async-credential-prompting |
| description | How to implement async credential prompting with proper error handling across multiple implementations (console, MCP elicitation, etc.) |
| domain | auth, async-patterns, error-handling |
| confidence | high |
| source | earned — fixing MCP auth sync-over-async deadlock issue |
When building authentication systems that need to prompt users for credentials, you may have multiple implementations (interactive console, MCP elicitation, web forms, etc.). Some are inherently sync (Console.ReadLine), others are async (network calls, IPC).
Key principle: Make the interface async. Don't sync-over-async in shared abstractions.
This skill applies when:
.GetAwaiter().GetResult()public interface ICredentialPrompt
{
Task<(string Email, string Password, string TotpCode)> PromptCredentialsAsync(CancellationToken ct = default);
}
Why: Even if one implementation is sync, the interface should be async if any implementation needs it. Pass CancellationToken to respect cancellation throughout the stack.
public sealed class ConsoleCredentialPrompt : ICredentialPrompt
{
public Task<(string Email, string Password, string TotpCode)> PromptCredentialsAsync(CancellationToken ct = default)
{
// ... synchronous Console.ReadLine() calls ...
var email = Console.ReadLine()?.Trim() ?? "";
var password = ReadMasked();
var totpCode = Console.ReadLine()?.Trim() ?? "";
return Task.FromResult((email, password, totpCode));
}
}
Why: Task.FromResult creates a completed task with no thread blocking. No performance penalty for sync operations.
public sealed class McpCredentialPrompt(McpServer mcpServer) : ICredentialPrompt
{
public async Task<(string Email, string Password, string TotpCode)> PromptCredentialsAsync(CancellationToken ct = default)
{
var requestParams = new ElicitRequestParams { /* schema */ };
try
{
var result = await mcpServer.ElicitAsync(requestParams, ct);
if (!result.IsAccepted || result.Content is null)
throw new InvalidOperationException("Authentication cancelled — credentials are required.");
// Extract fields from result...
return (email, password, totpCode);
}
catch (Exception ex) when (ex is not InvalidOperationException)
{
throw new InvalidOperationException(
"Authentication required but prompting failed. " +
"Run the CLI first to create a session, or ensure your client supports elicitation. " +
$"Details: {ex.Message}", ex);
}
}
}
Why:
InvalidOperationException) from infrastructure failuresprivate async Task ColdStartAsync(CancellationToken ct)
{
logger.LogDebug("Starting cold authentication...");
var (email, password, totpCode) = await credentialPrompt.PromptCredentialsAsync(ct);
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(totpCode))
throw new AuthenticationException("Email, password, and TOTP code are all required.");
// ... proceed with auth flow ...
}
Why: Async all the way — no sync-over-async anywhere in the call chain.
Project files:
src/FinaryExport.Core/Auth/ICredentialPrompt.cssrc/FinaryExport/ConsoleCredentialPrompt.cssrc/FinaryExport.Mcp/McpCredentialPrompt.cssrc/FinaryExport.Core/Auth/ClerkAuthClient.cs (line 119)Test mock setup:
var prompt = new Mock<ICredentialPrompt>();
prompt.Setup(p => p.PromptCredentialsAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(("test@example.com", "password", "123456"));
public interface ICredentialPrompt
{
(string, string, string) PromptCredentials(); // SYNC
}
public class McpCredentialPrompt : ICredentialPrompt
{
public (string, string, string) PromptCredentials()
{
// SYNC-OVER-ASYNC — risk of deadlocks!
return ElicitCredentialsAsync().GetAwaiter().GetResult();
}
private async Task<(string, string, string)> ElicitCredentialsAsync() { ... }
}
Problem: Sync-over-async can deadlock if there's a SynchronizationContext (ASP.NET, WPF, etc.). Even without one, it blocks a thread unnecessarily.
public interface ICredentialPrompt { ... }
public interface IAsyncCredentialPrompt { ... }
Problem: Complexity for no gain. Callers need to handle both. Just make one async interface and wrap sync implementations with Task.FromResult.
catch (Exception ex)
{
throw new InvalidOperationException("Authentication failed", ex);
}
Problem: Doesn't guide users to action. Better: "Run CLI first to create session.dat, or ensure MCP client supports elicitation."
EnsureInitializedAsync(), not just data methods. See AutoInitFinaryApiClient for complete coverage.