Skip to content

Add scoped chat MCP tool#58

Open
mhmdaskari wants to merge 7 commits into
Zen4-bit:mainfrom
mhmdaskari:feature-scoped-chat-mcp
Open

Add scoped chat MCP tool#58
mhmdaskari wants to merge 7 commits into
Zen4-bit:mainfrom
mhmdaskari:feature-scoped-chat-mcp

Conversation

@mhmdaskari

Copy link
Copy Markdown

Summary

Closes #57.

Adds a generic ask_scoped_chat MCP tool for sending messages inside existing project-like provider workspaces through the logged-in embedded browser session.

What changed

  • Added scoped URL validation and identity matching for ChatGPT Projects, Claude Projects, Perplexity Spaces, and Gemini Gems.
  • Added a queued scopedChat path that reuses existing IPC actions: provider init, navigation, uploads, send-button readiness, DOM-mode send, and response capture.
  • Kept existing provider tools unchanged.
  • Documented the new tool and examples in the README.

Why

Regular provider tools are useful for ordinary chats, but project-like workspaces have saved instructions, files, and knowledge that apply only inside the scoped provider page. This tool intentionally uses DOM mode instead of API-first sending so the provider page keeps that workspace context.

Validation

  • npm ci
  • node --check src/mcp-server-v3.js
  • node --check electron/main-v2.cjs
  • timeout 5s node src/mcp-server-v3.js </dev/null

The MCP startup probe reached MCP Server running; it warned that Agent Hub was not listening on 127.0.0.1:19222, which is expected when the Electron app is not running during this check.

Copilot AI review requested due to automatic review settings June 15, 2026 22:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new MCP tool to send messages (and optional file uploads) into an existing provider “scoped workspace” (ChatGPT Projects, Claude Projects, Perplexity Spaces, Gemini Gems) via the logged-in browser session.

Changes:

  • Introduce scoped-chat URL parsing/normalization helpers and local file path resolution for uploads.
  • Add AIProvider.scopedChat() with queueing, navigation-to-scope, optional uploads, and DOM-mode sending.
  • Document the new ask_scoped_chat tool and provide example payloads.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/mcp-server-v3.js Implements scoped-chat URL parsing, file resolution, provider method + MCP tool wiring, and result formatting.
README.md Documents ask_scoped_chat and provides provider-specific examples.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mcp-server-v3.js
Comment thread src/mcp-server-v3.js
Comment thread src/mcp-server-v3.js
Comment thread src/mcp-server-v3.js Outdated
Comment thread src/mcp-server-v3.js Outdated
Comment thread src/mcp-server-v3.js
@mhmdaskari
mhmdaskari marked this pull request as ready for review June 15, 2026 22:46
@qodo-code-review

qodo-code-review Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stale API cache on DOM send ✓ Resolved 🐞 Bug ≡ Correctness
Description
AIProvider._doScopedChat() sends with forceDOM=true then calls getResponseWithTyping, but Electron’s
getResponseWithTypingStatus() always returns any existing _apiResponseCache first. If the cache was
populated by a prior API-backed request (e.g., REST queryProvider path that returns the API response
directly without consuming the cache), ask_scoped_chat can return an unrelated earlier response
instead of the scoped chat reply.
Code

src/mcp-server-v3.js[R551-556]

+        console.error(`[${this.name}] Sending scoped chat message with DOM mode...`);
+        await this.ipc.send('sendMessage', this.name, { message, forceDOM: true });
+
+        console.error(`[${this.name}] Waiting for scoped chat response...`);
+        const result = await this.ipc.send('getResponseWithTyping', this.name, {});
+        const response = result.response || 'No response received';
Evidence
The scoped chat flow uses forceDOM then immediately fetches the response via getResponseWithTyping.
Electron caches API responses in _apiResponseCache and getResponseWithTypingStatus returns that
cache before doing any DOM scraping. REST queryProvider can return the API response directly without
calling getResponseWithTyping, which leaves _apiResponseCache populated and therefore available to
be mistakenly returned for a later forceDOM send.

src/mcp-server-v3.js[493-556]
electron/main-v2.cjs[732-757]
electron/main-v2.cjs[1248-1261]
electron/rest-api.cjs[233-255]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ask_scoped_chat` uses DOM mode (`forceDOM=true`) but still retrieves responses via `getResponseWithTyping`. In Electron, `getResponseWithTypingStatus()` prefers `_apiResponseCache[provider]` if present. Because DOM-mode sends do not clear this cache, a stale API response from a previous request can be returned.
### Issue Context
- API-first sends store `_apiResponseCache[provider]`.
- Some call paths (e.g. REST `queryProvider`) may use the API response directly and skip `getResponseWithTyping`, leaving `_apiResponseCache` populated.
- A subsequent DOM-mode send (`forceDOM=true`) will not overwrite or clear the cache, so `getResponseWithTyping` can return the stale cached response.
### Fix Focus Areas
- electron/main-v2.cjs[732-757]
- electron/main-v2.cjs[1248-1261]
- electron/rest-api.cjs[233-255]
### Suggested fix
Implement one of the following (preferred options first):
1. **In `sendMessageToProvider(provider, message, forceDOM)`**, when `forceDOM === true`, proactively clear any stale cache for that provider:
 - `delete _apiResponseCache[provider];`
 This guarantees DOM-mode sends will never be answered by an unrelated cached API response.
2. Alternatively, add an explicit flag to bypass API cache in `getResponseWithTypingStatus` (e.g. `ignoreApiCache`) and pass it from DOM-mode workflows.
Optional hardening:
- In REST `queryProvider`, if you choose to keep `_apiResponseCache` behavior as-is, clear `_apiResponseCache[provider]` after using `sendResult.result.response` so later `getResponseWithTyping` calls can’t consume an old response.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Scope identity not canonical ✓ Resolved 🐞 Bug ≡ Correctness
Description
scopedDetailsForUrl() bakes the matched path segment (e.g. project vs projects) into the computed
identity, so equivalent scope URLs can fail to match and ask_scoped_chat(newChat=false) will keep
navigating instead of reusing the current scope. This is likely when providers redirect between
singular/plural routes or users copy different URL variants.
Code

src/mcp-server-v3.js[R268-367]

+const SCOPED_CHAT_PROVIDERS = {
+    chatgpt: {
+        label: 'ChatGPT Project',
+        host: 'chatgpt.com',
+        scopeSegments: ['g']
+    },
+    claude: {
+        label: 'Claude Project',
+        host: 'claude.ai',
+        scopeSegments: ['project', 'projects']
+    },
+    perplexity: {
+        label: 'Perplexity Space',
+        host: 'perplexity.ai',
+        scopeSegments: ['space', 'spaces']
+    },
+    gemini: {
+        label: 'Gemini Gem',
+        host: 'gemini.google.com',
+        scopeSegments: ['gem', 'gems']
+    }
+};
+
+function getScopedChatConfig(provider) {
+    const config = SCOPED_CHAT_PROVIDERS[provider];
+    if (!config) {
+        throw new Error(`Unsupported scoped chat provider: ${provider}`);
+    }
+    return config;
+}
+
+function hostMatches(actualHost, allowedHost) {
+    const host = actualHost.toLowerCase();
+    const allowed = allowedHost.toLowerCase();
+    return host === allowed || host.endsWith(`.${allowed}`);
+}
+
+function parseProviderScopedUrl(provider, rawUrl) {
+    const config = getScopedChatConfig(provider);
+    let url;
+
+    try {
+        url = new URL(rawUrl);
+    } catch {
+        throw new Error(`Invalid ${config.label} URL: ${rawUrl}`);
+    }
+
+    if (!['http:', 'https:'].includes(url.protocol)) {
+        throw new Error(`${config.label} URL must use http or https`);
+    }
+
+    if (!hostMatches(url.hostname, config.host)) {
+        throw new Error(`${config.label} URL must be on ${config.host}`);
+    }
+
+    const details = scopedDetailsForUrl(provider, url);
+    if (!details.identity) {
+        throw new Error(`${config.label} URL must point to an existing ${config.label.toLowerCase()}`);
+    }
+
+    return { url, landingUrl: details.landingUrl, identity: details.identity, config };
+}
+
+function scopedIdentityForUrl(provider, urlLike) {
+    return scopedDetailsForUrl(provider, urlLike).identity;
+}
+
+function scopedDetailsForUrl(provider, urlLike) {
+    let url;
+    try {
+        url = urlLike instanceof URL ? urlLike : new URL(urlLike);
+    } catch {
+        return { identity: '', landingUrl: null };
+    }
+
+    const config = SCOPED_CHAT_PROVIDERS[provider];
+    if (!config || !hostMatches(url.hostname, config.host)) {
+        return { identity: '', landingUrl: null };
+    }
+
+    const segments = url.pathname.split('/').filter(Boolean);
+    for (const scopeSegment of config.scopeSegments) {
+        const index = segments.indexOf(scopeSegment);
+        if (index >= 0 && segments[index + 1]) {
+            const scopeId = segments[index + 1];
+            const landingUrl = new URL(url.href);
+            landingUrl.hash = '';
+            landingUrl.search = '';
+
+            if (provider === 'chatgpt' && scopeSegment === 'g') {
+                landingUrl.pathname = `/g/${scopeId}/project`;
+                landingUrl.search = '?tab=chats';
+            } else {
+                landingUrl.pathname = `/${scopeSegment}/${scopeId}`;
+            }
+
+            return {
+                identity: `${provider}:${config.host}/${scopeSegment}/${scopeId.toLowerCase()}`,
+                landingUrl
+            };
Evidence
The config explicitly allows multiple alias segments per provider, but the identity string includes
the specific matched segment; the identity is then used for equality checks to decide whether to
navigate.

src/mcp-server-v3.js[268-288]
src/mcp-server-v3.js[348-367]
src/mcp-server-v3.js[489-492]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scopedDetailsForUrl()` computes `identity` using the specific matched `scopeSegment` from `scopeSegments`. Because `scopeSegments` includes alias forms (`project`/`projects`, `space`/`spaces`, `gem`/`gems`), the *same* underlying scope can produce different identities, causing `currentIdentity !== scope.identity` and unnecessary navigation even when already inside the correct scope.
### Issue Context
This affects `ask_scoped_chat` when `newChat=false` and the current page URL differs only by an alias segment (or the provider redirects to a different alias).
### Fix Focus Areas
- src/mcp-server-v3.js[268-289]
- src/mcp-server-v3.js[348-367]
### Suggested direction
- Introduce a canonical segment per provider (e.g., always plural) and normalize both:
- the `identity` string
- the `landingUrl.pathname`
- Alternatively, map alias segments to a canonical token for identity (e.g., `scopeType='project'|'space'|'gem'`), independent of the URL alias that matched.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Upload failures misreported ✓ Resolved 🐞 Bug ☼ Reliability
Description
_doScopedChat() records each upload attempt and reports filesUploaded as the number of attempted
uploads without checking whether uploadFile actually succeeded. As a result, ask_scoped_chat can
claim files were uploaded even when uploads are disabled or failed, and it still sends the message
afterward.
Code

src/mcp-server-v3.js[R502-536]

+        const uploaded = [];
+        for (const filePath of uploadFiles) {
+            console.error(`[${this.name}] Uploading scoped chat file: ${filePath}`);
+            const uploadResult = await this.ipc.send('uploadFile', this.name, { filePath });
+            uploaded.push({ filePath, result: uploadResult });
+            await this.sleep(Math.max(0, uploadSettleMs));
+            await this.ipc.send('waitForSendButton', this.name, {});
+        }
+
+        let typingCheck = await this.getTypingStatus();
+        let waitCount = 0;
+        while (typingCheck.isTyping && waitCount < 5) {
+            console.error(`[${this.name}] AI still typing, waiting before scoped chat send...`);
+            await this.sleep(1000);
+            typingCheck = await this.getTypingStatus();
+            waitCount++;
+        }
+
+        console.error(`[${this.name}] Sending scoped chat message with DOM mode...`);
+        await this.ipc.send('sendMessage', this.name, { message, forceDOM: true });
+
+        console.error(`[${this.name}] Waiting for scoped chat response...`);
+        const result = await this.ipc.send('getResponseWithTyping', this.name, {});
+        const response = result.response || 'No response received';
+
+        return {
+            scopedChat: true,
+            provider: this.name,
+            scope: scope.config.label,
+            scopeUrl: scope.landingUrl.href,
+            currentUrl: await this.currentUrl(),
+            newChat,
+            filesUploaded: uploaded.length,
+            response
+        };
Evidence
Scoped chat unconditionally counts upload attempts as uploads, but the IPC layer can return failure
objects for uploadFile (e.g. when fileReferenceEnabled is off), meaning the count can be wrong and
failures are not surfaced clearly.

src/mcp-server-v3.js[502-536]
electron/main-v2.cjs[523-534]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_doScopedChat()` pushes every `uploadFile` result into `uploaded` and returns `filesUploaded: uploaded.length` regardless of whether `uploadResult.success` is false. Electron IPC explicitly returns `{ success: false, error: ... }` when file uploads are disabled or fail.
### Issue Context
This can silently drop intended attachments while still sending the message, and the tool output misleads the caller by reporting successful-looking upload counts.
### Fix Focus Areas
- src/mcp-server-v3.js[502-536]
- electron/main-v2.cjs[523-534]
### Suggested direction
- Check `uploadResult.success` after each upload:
- If false: either throw (fail fast) or collect `uploadErrors` and return them.
- Only increment `filesUploaded` for successful uploads.
- If `files` is non-empty and uploads are disabled, consider returning a tool error before sending the message (clear, immediate feedback).
- Optionally return `uploaded` details (or summarized errors) so callers can see which file(s) failed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Gemini landing URL not canonical ✓ Resolved 🐞 Bug ☼ Reliability
Description
For Gemini gemId-based scope detection, scopedDetailsForUrl() returns landingUrl as the original URL
(including any hash/query) instead of clearing them like the segment-based paths. This makes
navigation targets non-canonical and inconsistent with other providers’ scoping behavior.
Code

src/mcp-server-v3.js[R371-377]

+    const gemId = url.searchParams.get('gem') || url.searchParams.get('gemId');
+    if (provider === 'gemini' && gemId) {
+        return {
+            identity: `${provider}:${config.host}/gem/${gemId.toLowerCase()}`,
+            landingUrl: url
+        };
+    }
Evidence
Segment-based scope extraction clears hash/search and rewrites pathname to a canonical landing
URL, but the Gemini query-param branch returns the full original URL unchanged.

src/mcp-server-v3.js[353-363]
src/mcp-server-v3.js[371-377]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `scopedDetailsForUrl()`, the Gemini `gem`/`gemId` query-param branch returns `landingUrl: url` without stripping unrelated query params or hash, while other branches clear `hash`/`search` and rewrite the path to a canonical landing route.
### Issue Context
This is a canonicalization inconsistency that can cause unexpected navigation targets or make debugging harder.
### Fix Focus Areas
- src/mcp-server-v3.js[353-377]
### Suggested direction
- Construct a canonical Gemini landing URL for this branch (similar to the segment branches):
- `const landingUrl = new URL(url.origin + url.pathname)` (or a known canonical pathname)
- `landingUrl.search = '?gem=' + gemId` (or whichever minimal param is required)
- `landingUrl.hash = ''`
- Keep the identity logic unchanged, but ensure navigation uses the canonical landing URL.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add ask_scoped_chat MCP tool for project-scoped provider workspaces
✨ Enhancement 📝 Documentation 🕐 20-40 Minutes

Grey Divider

Walkthroughs

Description
• Add ask_scoped_chat tool to send messages inside existing provider workspaces via DOM mode.
• Validate scoped workspace URLs and match identity before reusing the logged-in browser session.
• Document scoped chat usage and examples for ChatGPT, Claude, Perplexity, and Gemini.
Diagram
graph TD
A{{"ask_scoped_chat"}} --> B["mcp-server-v3"] --> C["AIProvider.scopedChat (queue)"] --> D["IPC actions (navigate/upload/send)"] --> E["Embedded browser session"] --> F["Scoped workspace page"]
B --> G["Scoped URL + identity validation"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Auto-detect provider from scopeUrl
  • ➕ Simpler tool input (no separate provider field)
  • ➕ Prevents mismatches between provider and URL host/path
  • ➖ Adds ambiguity when hosts/path patterns change or overlap
  • ➖ Harder to keep stable behavior across provider UI changes
2. Provider-specific scoped tools (ask_chatgpt_project, ask_claude_project, ...)
  • ➕ Clear per-provider schema and validation rules
  • ➕ Easier to tailor provider-specific navigation quirks
  • ➖ API surface area expands quickly and duplicates logic
  • ➖ Harder to maintain consistent behavior across providers
3. Persist scope identity as session state (implicit continuation)
  • ➕ Fewer navigations; can default to continuing the last scope
  • ➕ Improves usability for repeated calls within the same workspace
  • ➖ Statefulness increases surprise/risk when multiple clients share the same server/session
  • ➖ Requires explicit state management and reset semantics

Recommendation: The PR’s approach (explicit provider + validated scopeUrl, identity matching, and DOM-mode sending) is a solid default: it keeps workspace context correctly and reuses existing IPC primitives without expanding provider-specific tooling. If ergonomics become a pain point, consider optional provider auto-detection as a follow-up, but keep explicit provider as the safest primary path.

Grey Divider

File Changes

Enhancement (1)
mcp-server-v3.js Implement scoped chat tool with URL validation, queuing, and DOM-mode send +256/-0

Implement scoped chat tool with URL validation, queuing, and DOM-mode send

• Introduces cross-provider scoped URL parsing/validation with identity matching and a file resolver for uploads. Adds a queued 'AIProvider.scopedChat' flow that navigates (when needed), uploads files, forces DOM-mode sending, and captures responses, then registers the 'ask_scoped_chat' MCP tool and formats its output.

src/mcp-server-v3.js


Documentation (1)
README.md Document new ask_scoped_chat tool and provider-specific examples +41/-0

Document new ask_scoped_chat tool and provider-specific examples

• Adds 'ask_scoped_chat' to the tools table and documents when to use scoped chat. Provides JSON examples for ChatGPT Projects, Claude Projects, Perplexity Spaces, and Gemini Gems, including optional file uploads and 'newChat' behavior.

README.md


Grey Divider

Qodo Logo

- Canonicalize scope identity using the first configured segment so
  equivalent URLs (e.g. /project/<id> vs /projects/<id>) resolve to the
  same identity; ask_scoped_chat(newChat=false) now reuses the current
  scope instead of re-navigating.
- Only count successful file uploads. Check uploadResult.success, skip
  the post-upload settle/wait for failures, and surface failed uploads
  in the result instead of reporting every attempt as uploaded.
- Return a canonical /gem/<id> landing URL for the Gemini gem-ID query
  form, clearing hash/query to match the segment-based branch behavior.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread src/mcp-server-v3.js
Comment thread src/mcp-server-v3.js
Comment thread src/mcp-server-v3.js
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 20:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/mcp-server-v3.js
Comment thread src/mcp-server-v3.js Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 20:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/mcp-server-v3.js Outdated
Comment thread src/mcp-server-v3.js
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 20:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/mcp-server-v3.js
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 20:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@mhmdaskari

Copy link
Copy Markdown
Author

/agentic_review

Comment thread src/mcp-server-v3.js
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 145e530

ask_scoped_chat sends with forceDOM=true and then reads the result via
getResponseWithTyping, which prefers _apiResponseCache[provider] when
present. The forceDOM branch of sendMessageToProvider never cleared that
cache, so a stale API response left populated by a prior request (e.g. a
REST queryProvider that used sendResult.result.response directly and
skipped getResponseWithTyping) could be returned instead of the actual
DOM response.

Delete _apiResponseCache[provider] when forceDOM=true so DOM-mode sends
are always answered by the live DOM response. This closes the only
staleness vector: every non-DOM caller of getResponseWithTyping is
already preceded by an API-first send that refreshes or clears the cache.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add MCP tool for project-scoped provider chats

2 participants