authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-04 21:43:24-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-04 21:43:43-07:00
log5e5766a9141ea0439e3e4b414e88749d908626c5
tree21a5fce0d7ccefac3014c0215475d97d09affe79
parent135b25088bc13f6cd6476e2fe8eeed863512e669
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

Put Shale linking directly in MCP approval requests

Unlinked requests now offer Link account as their primary action and preserve the request through sign-in. Linked requests retain repository selection and explicit approval. Assisted-by: gpt-6.1-sol

1 files changed, 10 insertions(+), 7 deletions(-)

dashboard/web/pages/MCP.tsx+10-7
...@@ -33,6 +33,13 @@ export function MCP() {...@@ -33,6 +33,13 @@ export function MCP() {
33 };33 };
34 });34 });
35 onCleanup(() => events?.close());35 onCleanup(() => events?.close());
36 const linkShale = async () => {
37 setBusy(true);
38 try {
39 const result = await parseResponse(api.mcp.shale.$post({ json: { request: consent()?.scopes.includes("shale:read") ? request() || undefined : undefined } }));
40 window.location.assign(result.redirect);
41 } catch (error) { toast(reason(error)); setBusy(false); }
42 };
36 const answer = async (deny: boolean) => {43 const answer = async (deny: boolean) => {
37 setBusy(true);44 setBusy(true);
38 try {45 try {
...@@ -52,10 +59,10 @@ export function MCP() {...@@ -52,10 +59,10 @@ export function MCP() {
52 <div class="mcp-resources"><For each={details().resources}>{(resource) =>59 <div class="mcp-resources"><For each={details().resources}>{(resource) =>
53 <Checkbox checked={picked().includes(resource.id)} onChange={(checked) => setPicked(checked ? [...picked(), resource.id] : picked().filter((item) => item !== resource.id))}>{resource.name}</Checkbox>60 <Checkbox checked={picked().includes(resource.id)} onChange={(checked) => setPicked(checked ? [...picked(), resource.id] : picked().filter((item) => item !== resource.id))}>{resource.name}</Checkbox>
54 }</For></div>61 }</For></div>
55 <Show when={!details().linked}><p>Link your Shale account below to choose repositories.</p></Show>62 <Show when={!details().linked}><p>Link your Shale account to choose repositories.</p></Show>
56 <Show when={details().linked && !details().resources.length}><p>No resources are available to your account.</p></Show>63 <Show when={details().linked && !details().resources.length}><p>No resources are available to your account.</p></Show>
57 <Show when={details().scopes.includes("offline_access")}><p class="muted">This connection can refresh access without another sign-in.</p></Show>64 <Show when={details().scopes.includes("offline_access")}><p class="muted">This connection can refresh access without another sign-in.</p></Show>
58 <div class="mcp-actions"><button class="button" disabled={!picked().length || busy()} onClick={() => answer(false)}>Allow access</button>65 <div class="mcp-actions"><Show when={details().linked} fallback={<button class="button" disabled={busy()} onClick={linkShale}>Link account</button>}><button class="button" disabled={!picked().length || busy()} onClick={() => answer(false)}>Allow access</button></Show>
59 <button class="button secondary" disabled={busy()} onClick={() => answer(true)}>Decline</button></div>66 <button class="button secondary" disabled={busy()} onClick={() => answer(true)}>Decline</button></div>
60 </section>}67 </section>}
61 </Loaded>68 </Loaded>
...@@ -71,11 +78,7 @@ export function MCP() {...@@ -71,11 +78,7 @@ export function MCP() {
71 <Show when={data().shale} fallback={<p>Link your Shale account to grant clients access to its repositories.</p>}>78 <Show when={data().shale} fallback={<p>Link your Shale account to grant clients access to its repositories.</p>}>
72 {(shale) => <p>Account linked <Ago t={shale().linkedAt} /></p>}79 {(shale) => <p>Account linked <Ago t={shale().linkedAt} /></p>}
73 </Show>80 </Show>
74 <button class="button" disabled={busy()} onClick={async () => {81 <button class="button" disabled={busy()} onClick={linkShale}>{data().shale ? "Relink account" : "Link account"}</button>
75 setBusy(true);
76 try { const result = await parseResponse(api.mcp.shale.$post({ json: { request: consent()?.scopes.includes("shale:read") ? request() || undefined : undefined } })); window.location.assign(result.redirect); }
77 catch (error) { toast(reason(error)); setBusy(false); }
78 }}>{data().shale ? "Relink account" : "Link account"}</button>
79 <Show when={data().shale}><button class="button secondary" disabled={busy()} onClick={async () => {82 <Show when={data().shale}><button class="button secondary" disabled={busy()} onClick={async () => {
80 setBusy(true);83 setBusy(true);
81 try { await parseResponse(api.mcp.shale.$delete()); await refetch(); }84 try { await parseResponse(api.mcp.shale.$delete()); await refetch(); }