Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ A modern, scalable system for extracting and analyzing Microsoft Teams messages

The MCP server allows you to query and analyze Teams messages directly from Claude Desktop on your Mac.

**Quick Setup:**
**Install via Claude Desktop extension UI:**
```bash
cd mcp-server
./setup-claude.sh
bash scripts/build_claude_extension.sh
```
Open Claude Desktop → **Developer → Extensions → Install Extension** and select the generated `dist/claude-extension/teams-extractor-mcp.zip`. Provide your PostgreSQL connection string when prompted, then restart Claude Desktop.

Then restart Claude Desktop. See [mcp-server/README.md](mcp-server/README.md) for detailed instructions.
**Alternative CLI setup:** run `./setup-claude.sh` from `mcp-server/` to update Claude's configuration file directly. See [mcp-server/README.md](mcp-server/README.md) for detailed instructions.

> **Note:** The MCP server runs locally on your Mac, not in Docker, as it uses stdio communication with Claude Desktop.

Expand Down
24 changes: 24 additions & 0 deletions backend/routes/mcp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const { exec } = require('child_process');
const path = require('path');

// POST /api/mcp/install
router.post('/install', (req, res) => {

Copilot AI Nov 3, 2025

Copy link

Choose a reason for hiding this comment

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

Missing authentication/authorization check: the /api/mcp/install endpoint executes system commands without verifying the requester's identity or permissions. This endpoint should require authentication (e.g., the X-API-Key header that's now supported in the Chrome extension) to prevent unauthorized installation attempts.

Suggested change
router.post('/install', (req, res) => {
router.post('/install', (req, res) => {
// Check for API key in header
const apiKey = req.header('X-API-Key');
const expectedApiKey = process.env.API_KEY;
if (!apiKey || apiKey !== expectedApiKey) {
return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key.' });
}

Copilot uses AI. Check for mistakes.
const mcpServerDir = path.resolve(__dirname, '../../mcp-server');
const installScript = path.join(mcpServerDir, 'install.js');

console.log(`Executing MCP installation script: ${installScript}`);

Copilot AI Nov 3, 2025

Copy link

Choose a reason for hiding this comment

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

The install.js script is an ES module (as seen from mcp-server/package.json having \"type\": \"module\"), but it's being invoked with plain node. This may fail if the script relies on ES module features. Consider verifying the script can be executed this way, or document this requirement explicitly.

Suggested change
// NOTE: install.js is an ES module. Requires Node.js v14+ and mcp-server/package.json to have "type": "module".
// If you encounter issues, ensure your Node.js version supports ES modules.

Copilot uses AI. Check for mistakes.
exec(`node ${installScript}`, { cwd: mcpServerDir }, (error, stdout, stderr) => {

Copilot AI Nov 3, 2025

Copy link

Choose a reason for hiding this comment

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

Command injection vulnerability: the installScript path is constructed using user-controlled input indirectly through __dirname. While path.join mitigates directory traversal, using template string interpolation with exec is unsafe. Use execFile instead with the script path as the first argument to prevent shell injection, or at minimum use an array of arguments with spawn.

Copilot uses AI. Check for mistakes.
if (error) {
console.error(`Error executing install.js: ${error.message}`);
return res.status(500).json({ error: `Script execution failed: ${stderr || error.message}` });
}

console.log(`install.js stdout: ${stdout}`);
res.json({ message: `Installation script finished: ${stdout}` });
Comment on lines +16 to +20

Copilot AI Nov 3, 2025

Copy link

Choose a reason for hiding this comment

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

Information disclosure: returning raw stdout and stderr from the installation script in the response could expose sensitive system information (e.g., file paths, environment variables, database connection strings). Consider returning a generic success message or sanitizing the output before sending it to the client.

Suggested change
return res.status(500).json({ error: `Script execution failed: ${stderr || error.message}` });
}
console.log(`install.js stdout: ${stdout}`);
res.json({ message: `Installation script finished: ${stdout}` });
return res.status(500).json({ error: "Script execution failed." });
}
console.log(`install.js stdout: ${stdout}`);
res.json({ message: "Installation script finished successfully." });

Copilot uses AI. Check for mistakes.
});
});

module.exports = router;
2 changes: 2 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const messagesRoutes = require('./routes/messages');
const statsRoutes = require('./routes/stats');
const healthRoutes = require('./routes/health');
const extractionRoutes = require('./routes/extraction');
const mcpRoutes = require('./routes/mcp');

// Initialize Express app
const app = express();
Expand Down Expand Up @@ -78,6 +79,7 @@ app.use('/api/messages', messagesRoutes);
app.use('/api/stats', statsRoutes);
app.use('/api/health', healthRoutes);
app.use('/api/extraction', extractionRoutes);
app.use('/api/mcp', mcpRoutes);

// Root endpoint
app.get('/', (req, res) => {
Expand Down
197 changes: 156 additions & 41 deletions chrome-extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,99 @@ let extractorState = {
droppedMessages: 0,
};

const DEFAULT_API_URL = 'http://localhost:5000/api';

// Configuration
let config = {
apiUrl: 'http://localhost:5000/api',
apiUrl: DEFAULT_API_URL,
apiKey: '',
syncInterval: 60000, // 1 minute
};

function sanitizeApiUrl(url) {
if (!url || typeof url !== 'string') {
return '';
}
return url.trim().replace(/\/+$/, '');
}

function getBatchEndpoint(customUrl) {
const base = sanitizeApiUrl(customUrl) || config.apiUrl || DEFAULT_API_URL;
return `${base}/messages/batch`;
}

async function handleBatchSendRequest(payload = {}) {
const { messages, extractionId, metadata, apiUrl, apiKey } = payload;

if (!Array.isArray(messages) || messages.length === 0) {
throw new Error('No messages to send');
}

if (!extractionId) {
throw new Error('Missing extractionId for batch');
}

const endpoint = getBatchEndpoint(apiUrl);
const headers = {
'Content-Type': 'application/json',
};
const resolvedApiKey = apiKey || config.apiKey;
if (resolvedApiKey) {
headers['X-API-Key'] = resolvedApiKey;
}

let response;
try {
response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
messages,
extractionId,
metadata: metadata || {},
}),
});
} catch (error) {
const networkError = new Error(error?.message || 'Failed to reach backend');
networkError.isNetworkError = true;
throw networkError;
}

if (!response.ok) {
const text = await response.text();
const err = new Error(`HTTP ${response.status}: ${response.statusText}`);
err.status = response.status;
err.details = text.slice(0, 500);
throw err;
}

return response.json();
}

// Load config from storage
chrome.storage.sync.get(['apiUrl', 'syncInterval'], (result) => {
if (result.apiUrl) config.apiUrl = result.apiUrl;
if (result.syncInterval) config.syncInterval = result.syncInterval;
chrome.storage.sync.get(['apiUrl', 'syncInterval', 'apiKey'], (result) => {
if (result.apiUrl) config.apiUrl = sanitizeApiUrl(result.apiUrl);
if (result.apiKey) config.apiKey = result.apiKey;
if (result.syncInterval) config.syncInterval = Number(result.syncInterval);
});

chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync') {
return;
}

if (changes.apiUrl) {
config.apiUrl = sanitizeApiUrl(changes.apiUrl.newValue) || DEFAULT_API_URL;
}
if (changes.apiKey) {
config.apiKey = changes.apiKey.newValue || '';
}
if (changes.syncInterval) {
const value = Number(changes.syncInterval.newValue);
if (!Number.isNaN(value)) {
config.syncInterval = value;
}
}
});

/**
Expand All @@ -31,67 +114,98 @@ chrome.storage.sync.get(['apiUrl', 'syncInterval'], (result) => {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Background received message:', message.type);

// Use a single listener with a switch statement
switch (message.type) {
case 'EXTRACTOR_READY':
extractorState.isActive = true;
extractorState.lastSync = new Date().toISOString();
updateBadge('✓', 'green');
console.log('Extractor is ready and active');
sendResponse({ success: true });
break;

case 'SEND_BATCH':
handleBatchSendRequest(message.payload || {})
.then((result) => {
sendResponse({ success: true, result });
})
.catch((error) => {
sendResponse({
success: false,
error: error.message,
status: error.status || null,
details: error.details || null,
network: Boolean(error.isNetworkError)
});
});
return true; // Indicates that the response is sent asynchronously

case 'MESSAGES_SENT':
extractorState.totalMessages += message.count;
extractorState.lastSync = new Date().toISOString();
extractorState.lastError = null; // Clear error on success
extractorState.retrying = false;
updateBadge(extractorState.totalMessages.toString(), 'blue');
console.log(`✅ Sent ${message.count} messages. Total: ${extractorState.totalMessages}`);
sendResponse({ success: true });
break;

case 'EXTRACTION_ERROR':
extractorState.lastError = {
timestamp: new Date().toISOString(),
error: message.error,
retrying: message.retrying || false,
retryCount: message.retryCount || 0
};
extractorState.retrying = message.retrying || false;

// Track dropped messages
if (message.dropped) {
extractorState.droppedMessages += message.dropped;
}

// Only add to errors array if it's a permanent failure
if (!message.retrying) {
extractorState.errors.push({
timestamp: new Date().toISOString(),
error: message.error
});
// Keep only last 10 errors
if (extractorState.errors.length > 10) {
extractorState.errors = extractorState.errors.slice(-10);
}
}

// Update badge based on retry status
if (message.retrying) {
updateBadge('⏰', 'orange');
} else {
updateBadge('!', 'red');
}
console.error('Extraction error:', message);
handleExtractionError(message);
sendResponse({ success: true });
break;

case 'GET_STATE':
case 'GET_STATUS':
sendResponse(extractorState);
return true;
}
break;

sendResponse({ success: true });
return true;
default:
console.warn('Unknown message type:', message.type);
sendResponse({ success: false, error: 'Unknown message type' });
break;
}
// Return true for async sendResponse, but not for sync cases
if (message.type !== 'SEND_BATCH') {
return false;
}
Comment on lines +168 to +171

Copilot AI Nov 3, 2025

Copy link

Choose a reason for hiding this comment

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

Incorrect async response handling: returning false for all non-SEND_BATCH messages is wrong because those handlers already call sendResponse synchronously. The return value should be true for async operations, but since these are synchronous, this logic is inverted. Remove this conditional and return nothing (implicit undefined), or return true only for SEND_BATCH.

Suggested change
// Return true for async sendResponse, but not for sync cases
if (message.type !== 'SEND_BATCH') {
return false;
}

Copilot uses AI. Check for mistakes.
});

/**
* Handles logging and state updates for extraction errors.
*/
function handleExtractionError(message) {
extractorState.lastError = {
timestamp: new Date().toISOString(),
error: message.error,
retrying: message.retrying || false,
retryCount: message.retryCount || 0
};
extractorState.retrying = message.retrying || false;

if (message.dropped) {
extractorState.droppedMessages += message.dropped;
}

if (!message.retrying) {
extractorState.errors.push({
timestamp: new Date().toISOString(),
error: message.error
});
if (extractorState.errors.length > 10) {
extractorState.errors = extractorState.errors.slice(-10);
}
}

if (message.retrying) {
updateBadge('⏰', 'orange');
} else {
updateBadge('!', 'red');
}
console.error('Extraction error reported:', message);
}


/**
* Update extension badge
*/
Expand Down Expand Up @@ -156,7 +270,8 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
*/
async function checkBackendHealth() {
try {
const response = await fetch(`${config.apiUrl}/health`);
const healthUrl = `${sanitizeApiUrl(config.apiUrl || DEFAULT_API_URL)}/health`;
const response = await fetch(healthUrl);
return response.ok;
} catch (error) {
console.error('Backend health check failed:', error);
Expand Down
Loading
Loading