@gajae-code/coding-agent 0.11.6 → 0.11.8
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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -23,7 +23,6 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
|
|
|
23
23
|
- Do not invent problems; report no issues found when the plan passes.
|
|
24
24
|
- Escalate routing needs upward: planner for plan revision, the deep-interview skill for requirements gathering, architect for code analysis.
|
|
25
25
|
- For consensus planning, reject shallow alternatives, driver contradictions, vague risks, weak verification, missing acceptance criteria, or under-specified areas needing expansion before execution.
|
|
26
|
-
- Prefer practical sufficiency over endless logical completeness: do not demand preemptive logic for edge cases that are not actually observed or evidenced, yet still require extensibility and robustness when observability-based evidence shows the investment is worth the token and time.
|
|
27
26
|
</constraints>
|
|
28
27
|
|
|
29
28
|
<execution_loop>
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Answer
|
|
4
|
-
Do not use tools.
|
|
5
|
-
Do not ask follow-up questions.
|
|
6
|
-
Question:
|
|
7
|
-
{{question}}
|
|
8
|
-
</btw>
|
|
1
|
+
You are answering inside an ephemeral multi-turn side chat for the interactive session.
|
|
2
|
+
Use only the sanitized visible-text conversation scope and prior side-chat turns provided as messages.
|
|
3
|
+
Answer directly. Do not use tools or request hidden session state.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[SUGGESTION MODE: Suggest what the user might naturally type next into the coding agent.]
|
|
2
|
+
|
|
3
|
+
FIRST: Look at the user's recent messages and original request.
|
|
4
|
+
|
|
5
|
+
Your job is to predict what THEY would type - not what you think they should do.
|
|
6
|
+
|
|
7
|
+
THE TEST: Would they think "I was just about to type that"?
|
|
8
|
+
|
|
9
|
+
EXAMPLES:
|
|
10
|
+
User asked "fix the bug and run tests", bug is fixed → "run the tests"
|
|
11
|
+
After code written → "try it out"
|
|
12
|
+
Agent offers options → suggest the one the user would likely pick, based on conversation
|
|
13
|
+
Agent asks to continue → "yes" or "go ahead"
|
|
14
|
+
Task complete, obvious follow-up → "commit this" or "push it"
|
|
15
|
+
After error or misunderstanding → silence (let them assess/correct)
|
|
16
|
+
|
|
17
|
+
Be specific: "run the tests" beats "continue".
|
|
18
|
+
|
|
19
|
+
NEVER SUGGEST:
|
|
20
|
+
- Evaluative ("looks good", "thanks")
|
|
21
|
+
- Questions ("what about…?")
|
|
22
|
+
- Agent-voice ("Let me…", "I'll…", "Here's…")
|
|
23
|
+
- New ideas they didn't ask about
|
|
24
|
+
- Multiple sentences
|
|
25
|
+
|
|
26
|
+
Stay silent if the next step isn't obvious from what the user said.
|
|
27
|
+
|
|
28
|
+
Stay silent if a suggestion could be unsafe or inappropriate — including any sensitive topic (security incidents, credentials, harm, private data). Even when the user is doing legitimate security or cybersecurity work, do not predict potentially unsafe actions.
|
|
29
|
+
|
|
30
|
+
Format: 2-12 words, match the user's style. Or nothing.
|
|
31
|
+
|
|
32
|
+
Reply with ONLY the suggestion, no quotes or explanation.
|
|
@@ -19,12 +19,15 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
|
|
|
19
19
|
{{#unless subagent}}
|
|
20
20
|
<gjc-runtime>
|
|
21
21
|
<routing>
|
|
22
|
-
- Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony.
|
|
22
|
+
- Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony. Small verification needs do not turn a clear request into a planning workflow.
|
|
23
|
+
- Ambiguous implementation asks with a missing target, scope, acceptance criteria, or safety boundary require clarification or the appropriate planning workflow before mutation.
|
|
23
24
|
- Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
|
|
24
|
-
- Vague requirements use `/skill:deep-interview
|
|
25
|
+
- Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
|
|
26
|
+
- Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
|
|
25
27
|
- Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
|
|
26
28
|
- Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
|
|
27
|
-
- Active skills are authoritative:
|
|
29
|
+
- Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
|
|
30
|
+
- Before explicit execution approval, planning and interview workflows NEVER edit product source, run mutating shell commands, commit, push, open PRs, or delegate implementation.
|
|
28
31
|
</routing>
|
|
29
32
|
</gjc-runtime>
|
|
30
33
|
{{/unless}}
|
|
@@ -206,7 +209,6 @@ Guardrails? None. Restrictions? None. Freedom? Unlimited.
|
|
|
206
209
|
- **Do your homework first** - Read the files, search, and understand the context before asking
|
|
207
210
|
- **Don’t Stop at Analysis** - Once you’ve assessed the situation, take the next step (session, comment, merge, fix, cleanup) on your own
|
|
208
211
|
- **No Inflating Operational/Meta Issues** - Just because the backlog is empty doesn’t mean you should force operational follow-ups, verifiability checks, or meta issues. Only raise issues for actual repository problems; otherwise, stop.
|
|
209
|
-
- **Practical sufficiency over completeness** - Don’t add logic that preemptively handles edge cases you haven’t actually observed; endless logical completeness loses to practical sufficiency. Extensibility and robustness matter, but they must be observability-based and evidence-based—any engineering without underlying evidence that it’s worth the token and time is over-engineering.
|
|
210
212
|
- **Always maintain execution** - Do not end your turn with just reporting, summarizing, or analyzing. If an action is possible, execute it immediately during the same turn.
|
|
211
213
|
- **Always be in action mode** - The default stance is intervention, not observation. If you see something that needs cleaning up, clean it up; if you see a session that needs opening, open it; if you see a PR that needs blocking, block it; if you see a PR that needs merging, review it and merge it.
|
|
212
214
|
- **No begging for permission** - For obvious follow-up actions (cleaning up sessions, marking items as stale, fixing routing, updating documentation to reflect Doctrine), don’t ask—just do it.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks
|
|
1
|
+
Schedule a prompt to fire on a recurring cron schedule, or one-shot at the next match. Cron tasks re-run an agent prompt automatically on an interval when every firing is allowed to produce a normal assistant response, such as a reminder or a scheduled status report.
|
|
2
|
+
|
|
3
|
+
Cron is not a silent polling primitive. Every firing starts an agent turn, and hiding the injected cron message does not hide that turn's assistant response. For ongoing logs, file watching, or PR/CI polling that should report only meaningful state changes, use `monitor` with a script that writes a line only when there is an event to process, and set `persistent: true` so the monitor survives the first emitted event. Do not schedule a cron prompt that asks the agent to suppress routine polls; prompt wording cannot make a cron-triggered assistant turn reliably silent.
|
|
2
4
|
|
|
3
5
|
Use a single `op` field to select the operation:
|
|
4
6
|
|
|
@@ -3,6 +3,7 @@ Discover project and user runtime skills without loading full skill content.
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Searches only custom runtime skill locations: nearest project `.gjc/skills`; then, under the home directory, canonical `<config>/agent/skills`, configured legacy `<config>/skills`, and historical legacy `.gjc/skills`. `<config>` is the home-relative directory name from `GJC_CONFIG_DIR`, then `PI_CONFIG_DIR`, then `.gjc`; even an absolute-looking configured name is joined beneath `<home>`. Duplicate names use that exact precedence. Built-in, bundled, and internal workflow skills are intentionally excluded.
|
|
5
5
|
- Returns thin metadata only: name, description, source scope, path, and use conditions when present.
|
|
6
|
+
- When zero candidates are returned because discovery config is disabled (`skills.enabled`, `skills.enablePiProject`, `skills.enablePiUser`), the result carries a `notice` explaining which setting blocked the search — an empty result without a `notice` means the searched scopes genuinely contain no matching skills.
|
|
6
7
|
- To load a selected skill's full `SKILL.md`, invoke it through the existing `skill` tool with the exact `name` returned here.
|
|
7
8
|
</instruction>
|
|
8
9
|
|
|
@@ -15,15 +15,12 @@ import type {
|
|
|
15
15
|
MCPInitializeParams,
|
|
16
16
|
MCPInitializeResult,
|
|
17
17
|
MCPPrompt,
|
|
18
|
-
MCPPromptsListResult,
|
|
19
18
|
MCPRequestOptions,
|
|
20
19
|
MCPResource,
|
|
21
20
|
MCPResourceReadParams,
|
|
22
21
|
MCPResourceReadResult,
|
|
23
22
|
MCPResourceSubscribeParams,
|
|
24
|
-
MCPResourcesListResult,
|
|
25
23
|
MCPResourceTemplate,
|
|
26
|
-
MCPResourceTemplatesListResult,
|
|
27
24
|
MCPServerCapabilities,
|
|
28
25
|
MCPServerConfig,
|
|
29
26
|
MCPServerConnection,
|
|
@@ -43,6 +40,9 @@ const PROTOCOL_VERSION = "2025-03-26";
|
|
|
43
40
|
/** Default connection timeout in ms */
|
|
44
41
|
const CONNECTION_TIMEOUT_MS = 30_000;
|
|
45
42
|
|
|
43
|
+
const MAX_PAGINATION_PAGES = 100,
|
|
44
|
+
MAX_PAGINATION_ITEMS = 10_000;
|
|
45
|
+
|
|
46
46
|
/** Client info sent during initialization */
|
|
47
47
|
const CLIENT_INFO = {
|
|
48
48
|
name: "gjc-coding-agent",
|
|
@@ -109,6 +109,40 @@ function decodeToolsListResult(value: unknown): MCPToolsListResult {
|
|
|
109
109
|
return value.nextCursor === undefined ? { tools } : { tools, nextCursor: value.nextCursor };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
async function collectPaginated<T>(
|
|
113
|
+
connection: MCPServerConnection,
|
|
114
|
+
options: MCPRequestOptions | undefined,
|
|
115
|
+
method: string,
|
|
116
|
+
itemKey: string,
|
|
117
|
+
items: T[],
|
|
118
|
+
decode?: (value: unknown) => unknown,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const seenCursors = new Set<string>();
|
|
121
|
+
const failure = (detail: string) => new MCPExpectedFailure(new Error(`MCP ${method} pagination ${detail}`));
|
|
122
|
+
let cursor: string | undefined;
|
|
123
|
+
for (let page = 1; page <= MAX_PAGINATION_PAGES; page++) {
|
|
124
|
+
const value = await connection.transport.request<unknown>(method, cursor ? { cursor } : {}, options);
|
|
125
|
+
const result = decode ? decode(value) : value;
|
|
126
|
+
if (
|
|
127
|
+
!isRecord(result) ||
|
|
128
|
+
!Array.isArray(result[itemKey]) ||
|
|
129
|
+
(result.nextCursor !== undefined && typeof result.nextCursor !== "string")
|
|
130
|
+
)
|
|
131
|
+
throw new MCPExpectedFailure();
|
|
132
|
+
const nextCursor = result.nextCursor as string | undefined;
|
|
133
|
+
if (nextCursor && seenCursors.has(nextCursor)) throw failure("repeated a cursor");
|
|
134
|
+
const pageItems = result[itemKey] as T[];
|
|
135
|
+
const itemCount = items.length + pageItems.length;
|
|
136
|
+
if (itemCount > MAX_PAGINATION_ITEMS || (itemCount === MAX_PAGINATION_ITEMS && nextCursor))
|
|
137
|
+
throw failure("did not complete within the 10000-item budget");
|
|
138
|
+
items.push(...pageItems);
|
|
139
|
+
if (!nextCursor) return;
|
|
140
|
+
if (page === MAX_PAGINATION_PAGES) throw failure("did not complete within the 100-page budget");
|
|
141
|
+
seenCursors.add(nextCursor);
|
|
142
|
+
cursor = nextCursor;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
/**
|
|
113
147
|
* Default handler for standard MCP server-to-client requests.
|
|
114
148
|
* Handles `ping` and `roots/list`; rejects unknown methods with -32601.
|
|
@@ -292,18 +326,7 @@ export async function listTools(
|
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
const allTools: MCPToolDefinition[] = [];
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
do {
|
|
298
|
-
const params: Record<string, unknown> = {};
|
|
299
|
-
if (cursor) {
|
|
300
|
-
params.cursor = cursor;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const result = decodeToolsListResult(await connection.transport.request<unknown>("tools/list", params, options));
|
|
304
|
-
allTools.push(...result.tools);
|
|
305
|
-
cursor = result.nextCursor;
|
|
306
|
-
} while (cursor);
|
|
329
|
+
await collectPaginated(connection, options, "tools/list", "tools", allTools, decodeToolsListResult);
|
|
307
330
|
|
|
308
331
|
// Cache tools
|
|
309
332
|
connection.tools = allTools;
|
|
@@ -362,18 +385,7 @@ export async function listResources(
|
|
|
362
385
|
}
|
|
363
386
|
|
|
364
387
|
const allResources: MCPResource[] = [];
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
do {
|
|
368
|
-
const params: Record<string, unknown> = {};
|
|
369
|
-
if (cursor) {
|
|
370
|
-
params.cursor = cursor;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const result = await connection.transport.request<MCPResourcesListResult>("resources/list", params, options);
|
|
374
|
-
allResources.push(...result.resources);
|
|
375
|
-
cursor = result.nextCursor;
|
|
376
|
-
} while (cursor);
|
|
388
|
+
await collectPaginated(connection, options, "resources/list", "resources", allResources);
|
|
377
389
|
|
|
378
390
|
connection.resources = allResources;
|
|
379
391
|
return allResources;
|
|
@@ -395,22 +407,7 @@ export async function listResourceTemplates(
|
|
|
395
407
|
}
|
|
396
408
|
|
|
397
409
|
const allTemplates: MCPResourceTemplate[] = [];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
do {
|
|
401
|
-
const params: Record<string, unknown> = {};
|
|
402
|
-
if (cursor) {
|
|
403
|
-
params.cursor = cursor;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const result = await connection.transport.request<MCPResourceTemplatesListResult>(
|
|
407
|
-
"resources/templates/list",
|
|
408
|
-
params,
|
|
409
|
-
options,
|
|
410
|
-
);
|
|
411
|
-
allTemplates.push(...result.resourceTemplates);
|
|
412
|
-
cursor = result.nextCursor;
|
|
413
|
-
} while (cursor);
|
|
410
|
+
await collectPaginated(connection, options, "resources/templates/list", "resourceTemplates", allTemplates);
|
|
414
411
|
|
|
415
412
|
connection.resourceTemplates = allTemplates;
|
|
416
413
|
return allTemplates;
|
|
@@ -514,18 +511,7 @@ export async function listPrompts(
|
|
|
514
511
|
}
|
|
515
512
|
|
|
516
513
|
const allPrompts: MCPPrompt[] = [];
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
do {
|
|
520
|
-
const params: Record<string, unknown> = {};
|
|
521
|
-
if (cursor) {
|
|
522
|
-
params.cursor = cursor;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const result = await connection.transport.request<MCPPromptsListResult>("prompts/list", params, options);
|
|
526
|
-
allPrompts.push(...result.prompts);
|
|
527
|
-
cursor = result.nextCursor;
|
|
528
|
-
} while (cursor);
|
|
514
|
+
await collectPaginated(connection, options, "prompts/list", "prompts", allPrompts);
|
|
529
515
|
|
|
530
516
|
connection.prompts = allPrompts;
|
|
531
517
|
return allPrompts;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const MCP_MAX_CONTENT_BYTES = 16 * 1024 * 1024;
|
|
2
|
+
export const MCP_MAX_ERROR_BYTES = 16 * 1024;
|
|
3
|
+
export const MCP_MAX_SSE_REQUEST_MESSAGES = 1024;
|
|
4
|
+
export const MCP_MAX_SSE_BATCH_MESSAGES = 1024;
|
|
5
|
+
export const MCP_HTTP_TIMEOUT_MS = 30_000;
|
|
6
|
+
|
|
7
|
+
function ignoreCancellation(cancel: () => Promise<unknown>): void {
|
|
8
|
+
try {
|
|
9
|
+
void cancel().catch(() => {});
|
|
10
|
+
} catch {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cancelMCPStream(stream: ReadableStream<Uint8Array> | null): void {
|
|
14
|
+
if (stream) ignoreCancellation(() => stream.cancel());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ByteReader = Pick<ReadableStreamDefaultReader<Uint8Array>, "read">;
|
|
18
|
+
|
|
19
|
+
async function readChunk(reader: ByteReader, signal?: AbortSignal) {
|
|
20
|
+
if (!signal) return reader.read();
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const aborted = Promise.withResolvers<never>();
|
|
23
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
24
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
25
|
+
try {
|
|
26
|
+
return await Promise.race([reader.read(), aborted.promise]);
|
|
27
|
+
} finally {
|
|
28
|
+
signal.removeEventListener("abort", onAbort);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readMCPResponseText(response: Response, limit: number, truncate = false, signal?: AbortSignal) {
|
|
33
|
+
const declared = response.headers.get("Content-Length");
|
|
34
|
+
if (!truncate && declared && /^\d+$/.test(declared) && Number(declared) > limit) {
|
|
35
|
+
cancelMCPStream(response.body);
|
|
36
|
+
throw new Error("MCP response exceeds size limit");
|
|
37
|
+
}
|
|
38
|
+
if (!response.body) return "";
|
|
39
|
+
const reader = response.body.getReader();
|
|
40
|
+
let buffer = new Uint8Array(Math.min(limit, 64 * 1024));
|
|
41
|
+
let total = 0;
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { value, done } = await readChunk(reader, signal);
|
|
45
|
+
if (done) break;
|
|
46
|
+
if (value.length === 0) continue;
|
|
47
|
+
const remaining = limit - total;
|
|
48
|
+
if (value.length > remaining) {
|
|
49
|
+
if (truncate && remaining > 0) buffer.set(value.subarray(0, remaining), total);
|
|
50
|
+
ignoreCancellation(() => reader.cancel());
|
|
51
|
+
if (!truncate) throw new Error("MCP response exceeds size limit");
|
|
52
|
+
return `${new TextDecoder().decode(buffer)}\u2026`;
|
|
53
|
+
}
|
|
54
|
+
if (total + value.length > buffer.length) {
|
|
55
|
+
const grown = new Uint8Array(Math.min(limit, Math.max(total + value.length, buffer.length * 2)));
|
|
56
|
+
grown.set(buffer);
|
|
57
|
+
buffer = grown;
|
|
58
|
+
}
|
|
59
|
+
buffer.set(value, total);
|
|
60
|
+
total += value.length;
|
|
61
|
+
}
|
|
62
|
+
return new TextDecoder().decode(buffer.subarray(0, total));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
ignoreCancellation(() => reader.cancel());
|
|
65
|
+
throw error;
|
|
66
|
+
} finally {
|
|
67
|
+
try {
|
|
68
|
+
reader.releaseLock();
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Lightweight utilities for calling MCP servers directly via HTTP
|
|
5
5
|
* without maintaining persistent connections.
|
|
6
6
|
*/
|
|
7
|
+
// biome-ignore assist/source/organizeImports: Keep independent MCP security imports on separate merge anchors.
|
|
8
|
+
import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
|
|
7
9
|
import { logger } from "@gajae-code/utils";
|
|
8
10
|
|
|
9
11
|
/** Parse SSE response format (lines starting with "data: ") */
|
|
@@ -25,6 +27,15 @@ export function parseSSE(text: string): unknown {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
async function translateMCPTimeout<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
31
|
+
try {
|
|
32
|
+
return await operation;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (signal.aborted && error === signal.reason) throw new Error("MCP request timed out");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
/** JSON-RPC 2.0 response structure */
|
|
29
40
|
export interface JsonRpcResponse<T = unknown> {
|
|
30
41
|
jsonrpc: "2.0";
|
|
@@ -57,22 +68,25 @@ export async function callMCP<T = unknown>(
|
|
|
57
68
|
params: params ?? {},
|
|
58
69
|
};
|
|
59
70
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Accept: "application/json, text/event-stream",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const signal = AbortSignal.timeout(MCP_HTTP_TIMEOUT_MS);
|
|
72
|
+
const response = await translateMCPTimeout(
|
|
73
|
+
fetch(url, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal,
|
|
78
|
+
}),
|
|
79
|
+
signal,
|
|
80
|
+
);
|
|
68
81
|
|
|
69
82
|
if (!response.ok) {
|
|
83
|
+
cancelMCPStream(response.body);
|
|
70
84
|
const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
|
71
85
|
logger.error(errorMsg, { url, method, params });
|
|
72
86
|
throw new Error(errorMsg);
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
const text = await response
|
|
89
|
+
const text = await translateMCPTimeout(readMCPResponseText(response, MCP_MAX_CONTENT_BYTES, false, signal), signal);
|
|
76
90
|
const result = parseSSE(text) as JsonRpcResponse<T> | null;
|
|
77
91
|
|
|
78
92
|
if (!result) {
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as url from "node:url";
|
|
9
|
-
import type
|
|
9
|
+
import { isCanonicalMCPOAuthBinding, resolveMCPOAuthResourceOrigin, type TSchema } from "@gajae-code/ai";
|
|
10
10
|
import { logger } from "@gajae-code/utils";
|
|
11
11
|
import type { SourceMeta } from "../capability/types";
|
|
12
12
|
import * as configValue from "../config/resolve-config-value";
|
|
13
13
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
14
|
-
import type { AuthStorage } from "../session/auth-storage";
|
|
14
|
+
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
15
15
|
import {
|
|
16
16
|
connectToServer,
|
|
17
17
|
disconnectServer,
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
unsubscribeFromResources,
|
|
28
28
|
} from "./client";
|
|
29
29
|
import { loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
30
|
-
import { refreshMCPOAuthToken } from "./oauth-flow";
|
|
31
30
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
32
31
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
33
32
|
import type { MCPToolCache } from "./tool-cache";
|
|
@@ -1469,64 +1468,63 @@ export class MCPManager {
|
|
|
1469
1468
|
let resolved: MCPServerConfig = { ...config };
|
|
1470
1469
|
|
|
1471
1470
|
const auth = config.auth;
|
|
1472
|
-
if (auth?.type === "oauth"
|
|
1471
|
+
if (auth?.type === "oauth") {
|
|
1472
|
+
if (!auth.credentialId || !this.#authStorage || (config.type !== "http" && config.type !== "sse")) {
|
|
1473
|
+
throw new MCPExpectedFailure();
|
|
1474
|
+
}
|
|
1473
1475
|
const credentialId = auth.credentialId;
|
|
1474
1476
|
let credential = this.#authStorage.get(credentialId);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1477
|
+
const binding = credential?.type === "oauth" ? credential.mcpBinding : undefined;
|
|
1478
|
+
const endpointOrigin = resolveMCPOAuthResourceOrigin(config.url);
|
|
1479
|
+
if (
|
|
1480
|
+
credential?.type !== "oauth" ||
|
|
1481
|
+
!binding ||
|
|
1482
|
+
!isCanonicalMCPOAuthBinding(binding) ||
|
|
1483
|
+
binding.resourceOrigin !== endpointOrigin ||
|
|
1484
|
+
(auth.tokenUrl !== undefined && auth.tokenUrl !== binding.tokenEndpoint)
|
|
1485
|
+
) {
|
|
1486
|
+
throw new MCPExpectedFailure();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Proactive refresh: 5-minute buffer before expiry
|
|
1490
|
+
// Force refresh: on 401/403 auth errors (revoked tokens, clock skew, missing expires)
|
|
1491
|
+
const REFRESH_BUFFER_MS = 5 * 60_000;
|
|
1492
|
+
const shouldRefresh =
|
|
1493
|
+
forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
|
|
1494
|
+
if (shouldRefresh && credential.refresh) {
|
|
1495
|
+
let refreshedCredential: OAuthCredential | undefined;
|
|
1496
|
+
try {
|
|
1497
|
+
refreshedCredential = await this.#authStorage.forceRefreshOAuthCredential(credentialId, credential, {
|
|
1498
|
+
clientId: auth.clientId,
|
|
1499
|
+
clientSecret: auth.clientSecret,
|
|
1500
|
+
});
|
|
1501
|
+
} catch {
|
|
1502
|
+
if (forceRefresh) throw new MCPExpectedFailure();
|
|
1503
|
+
if (this.#toolsOnly) {
|
|
1504
|
+
logger.debug("MCP OAuth refresh failed");
|
|
1505
|
+
} else {
|
|
1506
|
+
logger.warn("MCP OAuth refresh failed, using existing token");
|
|
1505
1507
|
}
|
|
1506
1508
|
}
|
|
1507
1509
|
if (refreshedCredential) {
|
|
1508
|
-
|
|
1510
|
+
const refreshedBinding = refreshedCredential.mcpBinding;
|
|
1511
|
+
if (
|
|
1512
|
+
!refreshedBinding ||
|
|
1513
|
+
refreshedBinding.resourceOrigin !== binding.resourceOrigin ||
|
|
1514
|
+
refreshedBinding.tokenEndpoint !== binding.tokenEndpoint
|
|
1515
|
+
) {
|
|
1516
|
+
throw new MCPExpectedFailure();
|
|
1517
|
+
}
|
|
1509
1518
|
credential = refreshedCredential;
|
|
1510
1519
|
}
|
|
1511
|
-
|
|
1512
|
-
if (resolved.type === "http" || resolved.type === "sse") {
|
|
1513
|
-
resolved = {
|
|
1514
|
-
...resolved,
|
|
1515
|
-
headers: {
|
|
1516
|
-
...resolved.headers,
|
|
1517
|
-
Authorization: `Bearer ${credential.access}`,
|
|
1518
|
-
},
|
|
1519
|
-
};
|
|
1520
|
-
} else {
|
|
1521
|
-
resolved = {
|
|
1522
|
-
...resolved,
|
|
1523
|
-
env: {
|
|
1524
|
-
...resolved.env,
|
|
1525
|
-
OAUTH_ACCESS_TOKEN: credential.access,
|
|
1526
|
-
},
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
1520
|
}
|
|
1521
|
+
resolved = {
|
|
1522
|
+
...config,
|
|
1523
|
+
headers: {
|
|
1524
|
+
...config.headers,
|
|
1525
|
+
Authorization: `Bearer ${credential.access}`,
|
|
1526
|
+
},
|
|
1527
|
+
};
|
|
1530
1528
|
}
|
|
1531
1529
|
if (this.#toolsOnly && resolved.type === "stdio") {
|
|
1532
1530
|
resolved = { ...resolved, noInheritEnv: true };
|
|
@@ -8,29 +8,10 @@
|
|
|
8
8
|
import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callback-server";
|
|
9
9
|
import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
|
|
10
10
|
import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
|
|
11
|
-
import { MCPExpectedFailure } from "./types";
|
|
12
11
|
|
|
13
12
|
const DEFAULT_PORT = 3000;
|
|
14
13
|
const CALLBACK_PATH = "/callback";
|
|
15
14
|
const CALLBACK_BIND_HOSTNAME = "127.0.0.1";
|
|
16
|
-
type RefreshTokenResponse = {
|
|
17
|
-
access_token: string;
|
|
18
|
-
refresh_token?: string;
|
|
19
|
-
expires_in?: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function isRefreshTokenResponse(value: unknown): value is RefreshTokenResponse {
|
|
23
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
24
|
-
const payload = value as Record<string, unknown>;
|
|
25
|
-
return (
|
|
26
|
-
typeof payload.access_token === "string" &&
|
|
27
|
-
payload.access_token.length > 0 &&
|
|
28
|
-
(payload.refresh_token === undefined || typeof payload.refresh_token === "string") &&
|
|
29
|
-
(payload.expires_in === undefined ||
|
|
30
|
-
(typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in >= 0))
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
15
|
function isLoopbackHostname(hostname: string): boolean {
|
|
35
16
|
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
36
17
|
}
|
|
@@ -384,56 +365,3 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
384
365
|
}
|
|
385
366
|
}
|
|
386
367
|
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Refresh an MCP OAuth token using the standard refresh_token grant.
|
|
390
|
-
* Returns updated credentials; preserves the old refresh token if the server doesn't rotate it.
|
|
391
|
-
*/
|
|
392
|
-
export async function refreshMCPOAuthToken(
|
|
393
|
-
tokenUrl: string,
|
|
394
|
-
refreshToken: string,
|
|
395
|
-
clientId?: string,
|
|
396
|
-
clientSecret?: string,
|
|
397
|
-
): Promise<OAuthCredentials> {
|
|
398
|
-
const params = new URLSearchParams({
|
|
399
|
-
grant_type: "refresh_token",
|
|
400
|
-
refresh_token: refreshToken,
|
|
401
|
-
});
|
|
402
|
-
if (clientId) params.set("client_id", clientId);
|
|
403
|
-
if (clientSecret) params.set("client_secret", clientSecret);
|
|
404
|
-
|
|
405
|
-
let response: Response;
|
|
406
|
-
try {
|
|
407
|
-
response = await fetch(tokenUrl, {
|
|
408
|
-
method: "POST",
|
|
409
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
410
|
-
body: params.toString(),
|
|
411
|
-
});
|
|
412
|
-
} catch (error) {
|
|
413
|
-
throw new MCPExpectedFailure(error);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (!response.ok) {
|
|
417
|
-
let text: string;
|
|
418
|
-
try {
|
|
419
|
-
text = await response.text();
|
|
420
|
-
} catch (error) {
|
|
421
|
-
throw new MCPExpectedFailure(error);
|
|
422
|
-
}
|
|
423
|
-
throw new MCPExpectedFailure(new Error(`MCP OAuth refresh failed: ${response.status} ${text}`));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
let data: unknown;
|
|
427
|
-
try {
|
|
428
|
-
data = await response.json();
|
|
429
|
-
} catch {
|
|
430
|
-
throw new MCPExpectedFailure();
|
|
431
|
-
}
|
|
432
|
-
if (!isRefreshTokenResponse(data)) throw new MCPExpectedFailure();
|
|
433
|
-
const expiresIn = data.expires_in ?? 3600;
|
|
434
|
-
return {
|
|
435
|
-
access: data.access_token,
|
|
436
|
-
refresh: data.refresh_token ?? refreshToken,
|
|
437
|
-
expires: Date.now() + expiresIn * 1000,
|
|
438
|
-
};
|
|
439
|
-
}
|