@oh-my-pi/pi-coding-agent 13.5.2 → 13.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/package.json +7 -7
- package/src/cli/agents-cli.ts +138 -0
- package/src/cli/args.ts +4 -1
- package/src/cli.ts +1 -0
- package/src/commands/agents.ts +57 -0
- package/src/config/prompt-templates.ts +2 -1
- package/src/exa/mcp-client.ts +57 -2
- package/src/mcp/oauth-discovery.ts +111 -36
- package/src/modes/components/mcp-add-wizard.ts +1 -1
- package/src/modes/controllers/command-controller.ts +0 -32
- package/src/modes/controllers/extension-ui-controller.ts +52 -7
- package/src/modes/controllers/mcp-command-controller.ts +2 -2
- package/src/patch/hashline.ts +41 -0
- package/src/prompts/system/plan-mode-active.md +12 -11
- package/src/prompts/system/plan-mode-subagent.md +3 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/hashline.md +26 -69
- package/src/prompts/tools/task.md +2 -3
- package/src/session/agent-session.ts +73 -5
- package/src/task/index.ts +3 -1
- package/src/tools/ask.ts +83 -51
- package/src/tools/bash.ts +5 -1
- package/src/tools/index.ts +18 -0
- package/src/utils/prompt-format.ts +16 -18
- package/src/web/search/providers/gemini.ts +74 -33
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.5.4] - 2026-03-01
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `authServerUrl` field to `AuthDetectionResult` to capture OAuth server metadata from `Mcp-Auth-Server` headers
|
|
9
|
+
- Added `extractMcpAuthServerUrl()` function to parse and validate `Mcp-Auth-Server` URLs from error messages
|
|
10
|
+
- Added support for `/.well-known/oauth-protected-resource` discovery endpoint to resolve authorization servers
|
|
11
|
+
- Added recursive auth server discovery to follow `authorization_servers` references when discovering OAuth endpoints
|
|
12
|
+
|
|
13
|
+
- Added `omp agents unpack` CLI subcommand to export bundled subagent definitions to `~/.omp/agent/agents` by default, with `--project` support for `./.omp/agents`
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Enhanced `discoverOAuthEndpoints()` to accept optional `authServerUrl` parameter and query both auth server and resource server for OAuth metadata
|
|
17
|
+
- Improved OAuth metadata extraction to handle additional field name variations (`clientId`, `default_client_id`, `public_client_id`)
|
|
18
|
+
- Refactored OAuth endpoint discovery logic into reusable `findEndpoints()` helper for consistent metadata parsing across multiple sources
|
|
19
|
+
- Task subagents now strip inherited `AGENTS.md` context files and the task tool prompt no longer warns against repeating AGENTS guidance, aligning subagent context with explicit task inputs ([#233](https://github.com/can1357/oh-my-pi/issues/233))
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed MCP OAuth discovery to honor `Mcp-Auth-Server` metadata and resolve authorization endpoints from the declared auth server, restoring Figma MCP login URLs with `client_id` ([#235](https://github.com/can1357/oh-my-pi/issues/235))
|
|
24
|
+
|
|
25
|
+
## [13.5.3] - 2026-03-01
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Auto-include `ast_grep` and `ast_edit` tools when their text-based counterparts (`grep`, `edit`) are requested and the AST tools are enabled
|
|
30
|
+
- Enforced tool decision in plan mode—agent now requires calling either `ask` or `exit_plan_mode` when a turn ends without a required tool call
|
|
31
|
+
- Auto-correction of escaped tab indentation in edits (enabled by default, controllable via `PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS` environment variable)
|
|
32
|
+
- Warning when suspicious Unicode escape placeholder `\uDDDD` is detected in edit content
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Updated bash tool description to conditionally show `ast_grep` and `ast_edit` guidance based on tool availability in the session
|
|
37
|
+
- Replaced timeout-based cancellation with AbortSignal-based cancellation in the `ask` tool for more reliable user interaction handling
|
|
38
|
+
- Updated `ask` tool to distinguish between user-initiated cancellation and timeout-driven auto-selection, with only user cancellation aborting the turn
|
|
39
|
+
- Updated hashline documentation to clarify that `\t` in JSON represents a real tab character, not a literal backslash-t sequence
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Fixed race condition in dialog overlay handling where multiple concurrent resolutions could occur
|
|
44
|
+
- Cancelling the `ask` tool now aborts the current turn instead of returning a normal cancelled selection, while timeout-driven auto-cancel still returns without aborting
|
|
45
|
+
|
|
5
46
|
## [13.5.2] - 2026-03-01
|
|
6
47
|
|
|
7
48
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.5.
|
|
4
|
+
"version": "13.5.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.5.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.5.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.5.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.5.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.5.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.5.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.5.4",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.5.4",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.5.4",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.5.4",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.5.4",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.5.4",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents CLI command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles `omp agents unpack` for writing bundled agent definitions to disk.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { getAgentDir, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { YAML } from "bun";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { theme } from "../modes/theme/theme";
|
|
12
|
+
import { loadBundledAgents } from "../task/agents";
|
|
13
|
+
import type { AgentDefinition } from "../task/types";
|
|
14
|
+
|
|
15
|
+
export type AgentsAction = "unpack";
|
|
16
|
+
|
|
17
|
+
export interface AgentsCommandArgs {
|
|
18
|
+
action: AgentsAction;
|
|
19
|
+
flags: {
|
|
20
|
+
force?: boolean;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
dir?: string;
|
|
23
|
+
user?: boolean;
|
|
24
|
+
project?: boolean;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UnpackResult {
|
|
29
|
+
targetDir: string;
|
|
30
|
+
total: number;
|
|
31
|
+
written: string[];
|
|
32
|
+
skipped: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeStdout(line: string): void {
|
|
36
|
+
process.stdout.write(`${line}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveTargetDir(flags: AgentsCommandArgs["flags"]): string {
|
|
40
|
+
if (flags.dir && flags.dir.trim().length > 0) {
|
|
41
|
+
return path.resolve(getProjectDir(), flags.dir.trim());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (flags.user && flags.project) {
|
|
45
|
+
throw new Error("Choose either --user or --project, not both.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (flags.project) {
|
|
49
|
+
return path.resolve(getProjectDir(), ".omp", "agents");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return path.join(getAgentDir(), "agents");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toFrontmatter(agent: AgentDefinition): Record<string, unknown> {
|
|
56
|
+
const frontmatter: Record<string, unknown> = {
|
|
57
|
+
name: agent.name,
|
|
58
|
+
description: agent.description,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (agent.tools && agent.tools.length > 0) frontmatter.tools = agent.tools;
|
|
62
|
+
if (agent.spawns !== undefined) frontmatter.spawns = agent.spawns;
|
|
63
|
+
if (agent.model && agent.model.length > 0) frontmatter.model = agent.model;
|
|
64
|
+
if (agent.thinkingLevel) frontmatter["thinking-level"] = agent.thinkingLevel;
|
|
65
|
+
if (agent.output !== undefined) frontmatter.output = agent.output;
|
|
66
|
+
if (agent.blocking) frontmatter.blocking = true;
|
|
67
|
+
|
|
68
|
+
return frontmatter;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function serializeAgent(agent: AgentDefinition): string {
|
|
72
|
+
const frontmatter = YAML.stringify(toFrontmatter(agent), null, 2).trimEnd();
|
|
73
|
+
const body = agent.systemPrompt.trim();
|
|
74
|
+
return `---\n${frontmatter}\n---\n\n${body}\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function unpackBundledAgents(flags: AgentsCommandArgs["flags"]): Promise<UnpackResult> {
|
|
78
|
+
const targetDir = resolveTargetDir(flags);
|
|
79
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
80
|
+
|
|
81
|
+
const bundledAgents = [...loadBundledAgents()].sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
+
const written: string[] = [];
|
|
83
|
+
const skipped: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const agent of bundledAgents) {
|
|
86
|
+
const filePath = path.join(targetDir, `${agent.name}.md`);
|
|
87
|
+
if (!flags.force) {
|
|
88
|
+
try {
|
|
89
|
+
await fs.stat(filePath);
|
|
90
|
+
skipped.push(filePath);
|
|
91
|
+
continue;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (!isEnoent(error)) throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await Bun.write(filePath, serializeAgent(agent));
|
|
98
|
+
written.push(filePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
targetDir,
|
|
103
|
+
total: bundledAgents.length,
|
|
104
|
+
written,
|
|
105
|
+
skipped,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function runAgentsCommand(cmd: AgentsCommandArgs): Promise<void> {
|
|
110
|
+
switch (cmd.action) {
|
|
111
|
+
case "unpack": {
|
|
112
|
+
const result = await unpackBundledAgents(cmd.flags);
|
|
113
|
+
if (cmd.flags.json) {
|
|
114
|
+
writeStdout(JSON.stringify(result, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
writeStdout(chalk.bold(`Bundled agents: ${result.total}`));
|
|
119
|
+
writeStdout(chalk.dim(`Target directory: ${result.targetDir}`));
|
|
120
|
+
writeStdout(chalk.green(`${theme.status.success} Written: ${result.written.length}`));
|
|
121
|
+
if (result.skipped.length > 0) {
|
|
122
|
+
writeStdout(
|
|
123
|
+
chalk.yellow(
|
|
124
|
+
`${theme.status.warning} Skipped existing: ${result.skipped.length} (use --force to overwrite)`,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const filePath of result.written) {
|
|
130
|
+
writeStdout(chalk.dim(` + ${filePath}`));
|
|
131
|
+
}
|
|
132
|
+
for (const filePath of result.skipped) {
|
|
133
|
+
writeStdout(chalk.dim(` = ${filePath}`));
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/cli/args.ts
CHANGED
|
@@ -247,7 +247,10 @@ ${chalk.bold("Available Tools (all enabled by default):")}
|
|
|
247
247
|
fetch - Fetch and process URLs
|
|
248
248
|
web_search - Search the web
|
|
249
249
|
ask - Ask user questions (interactive mode only)
|
|
250
|
-
|
|
250
|
+
|
|
251
|
+
${chalk.bold("Useful Commands:")}
|
|
252
|
+
omp agents unpack - Export bundled subagents to ~/.omp/agent/agents (default)
|
|
253
|
+
omp agents unpack --project - Export bundled subagents to ./.omp/agents`;
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
export function printHelp(): void {
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ process.title = APP_NAME;
|
|
|
16
16
|
|
|
17
17
|
const commands: CommandEntry[] = [
|
|
18
18
|
{ name: "launch", load: () => import("./commands/launch").then(m => m.default) },
|
|
19
|
+
{ name: "agents", load: () => import("./commands/agents").then(m => m.default) },
|
|
19
20
|
{ name: "commit", load: () => import("./commands/commit").then(m => m.default) },
|
|
20
21
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
21
22
|
{ name: "grep", load: () => import("./commands/grep").then(m => m.default) },
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manage bundled task agents.
|
|
3
|
+
*/
|
|
4
|
+
import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
+
import { type AgentsAction, type AgentsCommandArgs, runAgentsCommand } from "../cli/agents-cli";
|
|
6
|
+
import { initTheme } from "../modes/theme/theme";
|
|
7
|
+
|
|
8
|
+
const ACTIONS: AgentsAction[] = ["unpack"];
|
|
9
|
+
|
|
10
|
+
export default class Agents extends Command {
|
|
11
|
+
static description = "Manage bundled task agents";
|
|
12
|
+
|
|
13
|
+
static args = {
|
|
14
|
+
action: Args.string({
|
|
15
|
+
description: "Agents action",
|
|
16
|
+
required: false,
|
|
17
|
+
options: ACTIONS,
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
static flags = {
|
|
22
|
+
force: Flags.boolean({ char: "f", description: "Overwrite existing agent files" }),
|
|
23
|
+
json: Flags.boolean({ description: "Output JSON" }),
|
|
24
|
+
dir: Flags.string({ description: "Output directory (overrides --user/--project)" }),
|
|
25
|
+
user: Flags.boolean({ description: "Write to ~/.omp/agent/agents (default)" }),
|
|
26
|
+
project: Flags.boolean({ description: "Write to ./.omp/agents" }),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
static examples = [
|
|
30
|
+
"# Export bundled agents into user config (default)\n omp agents unpack",
|
|
31
|
+
"# Export bundled agents into project config\n omp agents unpack --project",
|
|
32
|
+
"# Overwrite existing local agent files\n omp agents unpack --project --force",
|
|
33
|
+
"# Export into a custom directory\n omp agents unpack --dir ./tmp/agents --json",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
async run(): Promise<void> {
|
|
37
|
+
const { args, flags } = await this.parse(Agents);
|
|
38
|
+
if (!args.action) {
|
|
39
|
+
renderCommandHelp("omp", "agents", Agents);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cmd: AgentsCommandArgs = {
|
|
44
|
+
action: args.action as AgentsAction,
|
|
45
|
+
flags: {
|
|
46
|
+
force: flags.force,
|
|
47
|
+
json: flags.json,
|
|
48
|
+
dir: flags.dir,
|
|
49
|
+
user: flags.user,
|
|
50
|
+
project: flags.project,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
await initTheme();
|
|
55
|
+
await runAgentsCommand(cmd);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -255,7 +255,8 @@ handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectio
|
|
|
255
255
|
*/
|
|
256
256
|
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
|
|
257
257
|
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
|
|
258
|
-
const
|
|
258
|
+
const raw = typeof content === "string" ? content : String(content ?? "");
|
|
259
|
+
const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
259
260
|
const ref = `${num}#${computeLineHash(num, text)}`;
|
|
260
261
|
return { num, text, ref };
|
|
261
262
|
}
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -16,6 +16,61 @@ export function findApiKey(): string | null {
|
|
|
16
16
|
return $env.EXA_API_KEY;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
20
|
+
if (typeof value !== "object" || value === null) return null;
|
|
21
|
+
return value as Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseJsonContent(text: string): unknown | null {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize tools/call payloads across MCP servers.
|
|
34
|
+
*
|
|
35
|
+
* Exa currently returns different shapes depending on deployment/environment:
|
|
36
|
+
* - direct payload in result
|
|
37
|
+
* - structured payload under result.structuredContent / result.data / result.result
|
|
38
|
+
* - JSON payload embedded as text in result.content[]
|
|
39
|
+
*/
|
|
40
|
+
function normalizeMcpToolPayload(payload: unknown): unknown {
|
|
41
|
+
const candidates: unknown[] = [];
|
|
42
|
+
const root = asRecord(payload);
|
|
43
|
+
|
|
44
|
+
if (root) {
|
|
45
|
+
if (root.structuredContent !== undefined) candidates.push(root.structuredContent);
|
|
46
|
+
if (root.data !== undefined) candidates.push(root.data);
|
|
47
|
+
if (root.result !== undefined) candidates.push(root.result);
|
|
48
|
+
candidates.push(root);
|
|
49
|
+
|
|
50
|
+
const content = root.content;
|
|
51
|
+
if (Array.isArray(content)) {
|
|
52
|
+
for (const item of content) {
|
|
53
|
+
const part = asRecord(item);
|
|
54
|
+
if (!part) continue;
|
|
55
|
+
const text = part.text;
|
|
56
|
+
if (typeof text !== "string" || text.trim().length === 0) continue;
|
|
57
|
+
const parsed = parseJsonContent(text);
|
|
58
|
+
if (parsed !== null) candidates.push(parsed);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
candidates.push(payload);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (isSearchResponse(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
|
|
19
74
|
/** Fetch available tools from Exa MCP */
|
|
20
75
|
export async function fetchExaTools(apiKey: string | null, toolNames: string[]): Promise<MCPTool[]> {
|
|
21
76
|
const params = new URLSearchParams();
|
|
@@ -65,7 +120,7 @@ export async function callExaTool(
|
|
|
65
120
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
return response.result;
|
|
123
|
+
return normalizeMcpToolPayload(response.result);
|
|
69
124
|
}
|
|
70
125
|
|
|
71
126
|
/** Call a tool on Websets MCP */
|
|
@@ -85,7 +140,7 @@ export async function callWebsetsTool(
|
|
|
85
140
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
86
141
|
}
|
|
87
142
|
|
|
88
|
-
return response.result;
|
|
143
|
+
return normalizeMcpToolPayload(response.result);
|
|
89
144
|
}
|
|
90
145
|
|
|
91
146
|
/** Format search results for LLM */
|
|
@@ -16,9 +16,25 @@ export interface AuthDetectionResult {
|
|
|
16
16
|
requiresAuth: boolean;
|
|
17
17
|
authType?: "oauth" | "apikey" | "unknown";
|
|
18
18
|
oauth?: OAuthEndpoints;
|
|
19
|
+
authServerUrl?: string;
|
|
19
20
|
message?: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function parseMcpAuthServerUrl(errorMessage: string): string | undefined {
|
|
24
|
+
const match = errorMessage.match(/Mcp-Auth-Server:\s*([^;\]\s]+)/i);
|
|
25
|
+
if (!match || !match[1]) return undefined;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return new URL(match[1]).toString();
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function extractMcpAuthServerUrl(error: Error): string | undefined {
|
|
35
|
+
return parseMcpAuthServerUrl(error.message);
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
39
|
* Detect if an error indicates authentication is required.
|
|
24
40
|
* Checks for common auth error patterns.
|
|
@@ -178,6 +194,8 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
|
|
|
178
194
|
return { requiresAuth: false };
|
|
179
195
|
}
|
|
180
196
|
|
|
197
|
+
const authServerUrl = extractMcpAuthServerUrl(error);
|
|
198
|
+
|
|
181
199
|
// Try to extract OAuth endpoints
|
|
182
200
|
const oauth = extractOAuthEndpoints(error);
|
|
183
201
|
|
|
@@ -186,6 +204,7 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
|
|
|
186
204
|
requiresAuth: true,
|
|
187
205
|
authType: "oauth",
|
|
188
206
|
oauth,
|
|
207
|
+
authServerUrl,
|
|
189
208
|
message: "Server requires OAuth authentication. Launching authorization flow...",
|
|
190
209
|
};
|
|
191
210
|
}
|
|
@@ -201,6 +220,7 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
|
|
|
201
220
|
return {
|
|
202
221
|
requiresAuth: true,
|
|
203
222
|
authType: "apikey",
|
|
223
|
+
authServerUrl,
|
|
204
224
|
message: "Server requires API key authentication.",
|
|
205
225
|
};
|
|
206
226
|
}
|
|
@@ -209,6 +229,7 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
|
|
|
209
229
|
return {
|
|
210
230
|
requiresAuth: true,
|
|
211
231
|
authType: "unknown",
|
|
232
|
+
authServerUrl,
|
|
212
233
|
message: "Server requires authentication but type could not be determined.",
|
|
213
234
|
};
|
|
214
235
|
}
|
|
@@ -217,56 +238,110 @@ export function analyzeAuthError(error: Error): AuthDetectionResult {
|
|
|
217
238
|
* Try to discover OAuth endpoints by querying the server's well-known endpoints.
|
|
218
239
|
* This is a fallback when error responses don't include OAuth metadata.
|
|
219
240
|
*/
|
|
220
|
-
export async function discoverOAuthEndpoints(
|
|
241
|
+
export async function discoverOAuthEndpoints(
|
|
242
|
+
serverUrl: string,
|
|
243
|
+
authServerUrl?: string,
|
|
244
|
+
): Promise<OAuthEndpoints | null> {
|
|
221
245
|
const wellKnownPaths = [
|
|
222
246
|
"/.well-known/oauth-authorization-server",
|
|
223
247
|
"/.well-known/openid-configuration",
|
|
248
|
+
"/.well-known/oauth-protected-resource",
|
|
224
249
|
"/oauth/metadata",
|
|
225
250
|
"/.mcp/auth",
|
|
226
251
|
"/authorize", // Some MCP servers expose OAuth config here
|
|
227
252
|
];
|
|
253
|
+
const urlsToQuery = [authServerUrl, serverUrl].filter((value): value is string => Boolean(value));
|
|
254
|
+
const visitedAuthServers = new Set<string>();
|
|
228
255
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
256
|
+
const findEndpoints = (metadata: Record<string, unknown>): OAuthEndpoints | null => {
|
|
257
|
+
if (metadata.authorization_endpoint && metadata.token_endpoint) {
|
|
258
|
+
const scopesSupported = Array.isArray(metadata.scopes_supported)
|
|
259
|
+
? metadata.scopes_supported.filter((scope): scope is string => typeof scope === "string").join(" ")
|
|
260
|
+
: undefined;
|
|
261
|
+
return {
|
|
262
|
+
authorizationUrl: String(metadata.authorization_endpoint),
|
|
263
|
+
tokenUrl: String(metadata.token_endpoint),
|
|
264
|
+
clientId:
|
|
265
|
+
typeof metadata.client_id === "string"
|
|
266
|
+
? metadata.client_id
|
|
267
|
+
: typeof metadata.clientId === "string"
|
|
268
|
+
? metadata.clientId
|
|
269
|
+
: typeof metadata.default_client_id === "string"
|
|
270
|
+
? metadata.default_client_id
|
|
271
|
+
: typeof metadata.public_client_id === "string"
|
|
272
|
+
? metadata.public_client_id
|
|
273
|
+
: undefined,
|
|
274
|
+
scopes:
|
|
275
|
+
scopesSupported ||
|
|
276
|
+
(typeof metadata.scopes === "string"
|
|
277
|
+
? metadata.scopes
|
|
278
|
+
: typeof metadata.scope === "string"
|
|
279
|
+
? metadata.scope
|
|
280
|
+
: undefined),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
236
283
|
|
|
237
|
-
|
|
238
|
-
|
|
284
|
+
if (metadata.oauth || metadata.authorization || metadata.auth) {
|
|
285
|
+
const oauthData = (metadata.oauth || metadata.authorization || metadata.auth) as Record<string, unknown>;
|
|
286
|
+
if (typeof oauthData.authorization_url === "string" && typeof oauthData.token_url === "string") {
|
|
287
|
+
return {
|
|
288
|
+
authorizationUrl: oauthData.authorization_url || String(oauthData.authorizationUrl),
|
|
289
|
+
tokenUrl: oauthData.token_url || String(oauthData.tokenUrl),
|
|
290
|
+
clientId:
|
|
291
|
+
typeof oauthData.client_id === "string"
|
|
292
|
+
? oauthData.client_id
|
|
293
|
+
: typeof oauthData.clientId === "string"
|
|
294
|
+
? oauthData.clientId
|
|
295
|
+
: typeof oauthData.default_client_id === "string"
|
|
296
|
+
? oauthData.default_client_id
|
|
297
|
+
: typeof oauthData.public_client_id === "string"
|
|
298
|
+
? oauthData.public_client_id
|
|
299
|
+
: undefined,
|
|
300
|
+
scopes:
|
|
301
|
+
typeof oauthData.scopes === "string"
|
|
302
|
+
? oauthData.scopes
|
|
303
|
+
: typeof oauthData.scope === "string"
|
|
304
|
+
? oauthData.scope
|
|
305
|
+
: undefined,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
239
309
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
310
|
+
return null;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
for (const baseUrl of urlsToQuery) {
|
|
314
|
+
visitedAuthServers.add(baseUrl);
|
|
315
|
+
for (const path of wellKnownPaths) {
|
|
316
|
+
try {
|
|
317
|
+
const url = new URL(path, baseUrl);
|
|
318
|
+
const response = await fetch(url.toString(), {
|
|
319
|
+
method: "GET",
|
|
320
|
+
headers: { Accept: "application/json" },
|
|
321
|
+
});
|
|
250
322
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
323
|
+
if (response.ok) {
|
|
324
|
+
const metadata = (await response.json()) as Record<string, unknown>;
|
|
325
|
+
const endpoints = findEndpoints(metadata);
|
|
326
|
+
if (endpoints) return endpoints;
|
|
327
|
+
|
|
328
|
+
if (path === "/.well-known/oauth-protected-resource") {
|
|
329
|
+
const authServers = Array.isArray(metadata.authorization_servers)
|
|
330
|
+
? metadata.authorization_servers.filter((entry): entry is string => typeof entry === "string")
|
|
331
|
+
: [];
|
|
332
|
+
|
|
333
|
+
for (const discoveredAuthServer of authServers) {
|
|
334
|
+
if (visitedAuthServers.has(discoveredAuthServer)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const discovered = await discoverOAuthEndpoints(serverUrl, discoveredAuthServer);
|
|
338
|
+
if (discovered) return discovered;
|
|
339
|
+
}
|
|
265
340
|
}
|
|
266
341
|
}
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore errors, try next path
|
|
267
344
|
}
|
|
268
|
-
} catch {
|
|
269
|
-
// Ignore errors, try next path
|
|
270
345
|
}
|
|
271
346
|
}
|
|
272
347
|
|
|
@@ -947,7 +947,7 @@ export class MCPAddWizard extends Container {
|
|
|
947
947
|
let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
|
|
948
948
|
if (!oauth && this.#state.transport !== "stdio" && this.#state.url) {
|
|
949
949
|
try {
|
|
950
|
-
oauth = await discoverOAuthEndpoints(this.#state.url);
|
|
950
|
+
oauth = await discoverOAuthEndpoints(this.#state.url, authResult.authServerUrl);
|
|
951
951
|
} catch {
|
|
952
952
|
// Ignore discovery failures and fallback to manual auth.
|
|
953
953
|
}
|
|
@@ -983,19 +983,6 @@ function resolveAggregateStatus(limits: UsageLimit[]): UsageLimit["status"] {
|
|
|
983
983
|
return "exhausted";
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
-
function isZeroUsage(limit: UsageLimit): boolean {
|
|
987
|
-
const amount = limit.amount;
|
|
988
|
-
if (amount.usedFraction !== undefined) return amount.usedFraction <= 0;
|
|
989
|
-
if (amount.used !== undefined) return amount.used <= 0;
|
|
990
|
-
if (amount.unit === "percent" && amount.used !== undefined) return amount.used <= 0;
|
|
991
|
-
if (amount.remainingFraction !== undefined) return amount.remainingFraction >= 1;
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function isZeroUsageGroup(limits: UsageLimit[]): boolean {
|
|
996
|
-
return limits.length > 0 && limits.every(limit => isZeroUsage(limit));
|
|
997
|
-
}
|
|
998
|
-
|
|
999
986
|
function formatAggregateAmount(limits: UsageLimit[]): string {
|
|
1000
987
|
const fractions = limits
|
|
1001
988
|
.map(limit => resolveFraction(limit))
|
|
@@ -1117,13 +1104,6 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
|
|
|
1117
1104
|
}
|
|
1118
1105
|
}
|
|
1119
1106
|
|
|
1120
|
-
const providerAllZero = isZeroUsageGroup(Array.from(limitGroups.values()).flatMap(group => group.limits));
|
|
1121
|
-
if (providerAllZero) {
|
|
1122
|
-
const providerTitle = `${resolveStatusIcon("ok", uiTheme)} ${uiTheme.fg("accent", `${providerName} (0%)`)}`;
|
|
1123
|
-
lines.push(uiTheme.bold(providerTitle));
|
|
1124
|
-
continue;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
1107
|
lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
|
|
1128
1108
|
|
|
1129
1109
|
for (const group of limitGroups.values()) {
|
|
@@ -1144,18 +1124,6 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
|
|
|
1144
1124
|
|
|
1145
1125
|
const status = resolveAggregateStatus(sortedLimits);
|
|
1146
1126
|
const statusIcon = resolveStatusIcon(status, uiTheme);
|
|
1147
|
-
if (isZeroUsageGroup(sortedLimits)) {
|
|
1148
|
-
const resetText = resolveResetRange(sortedLimits, nowMs);
|
|
1149
|
-
const resetSuffix = resetText ? ` | ${resetText}` : "";
|
|
1150
|
-
const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
|
|
1151
|
-
lines.push(
|
|
1152
|
-
`${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix} ${uiTheme.fg(
|
|
1153
|
-
"dim",
|
|
1154
|
-
`0%${resetSuffix}`,
|
|
1155
|
-
)}`.trim(),
|
|
1156
|
-
);
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
1127
|
|
|
1160
1128
|
const windowSuffix = formatWindowSuffix(group.label, group.windowLabel, uiTheme);
|
|
1161
1129
|
lines.push(`${statusIcon} ${uiTheme.bold(group.label)} ${windowSuffix}`.trim());
|