diff --git a/README.md b/README.md index f8ed08d..6127e2e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/backend/routes/mcp.js b/backend/routes/mcp.js new file mode 100644 index 0000000..f75938c --- /dev/null +++ b/backend/routes/mcp.js @@ -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) => { + const mcpServerDir = path.resolve(__dirname, '../../mcp-server'); + const installScript = path.join(mcpServerDir, 'install.js'); + + console.log(`Executing MCP installation script: ${installScript}`); + + exec(`node ${installScript}`, { cwd: mcpServerDir }, (error, stdout, stderr) => { + 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}` }); + }); +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index 3c8632e..e4326b5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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(); @@ -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) => { diff --git a/chrome-extension/background.js b/chrome-extension/background.js index 3c428f7..4cfc3d0 100644 --- a/chrome-extension/background.js +++ b/chrome-extension/background.js @@ -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; + } + } }); /** @@ -31,14 +114,32 @@ 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(); @@ -46,52 +147,65 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 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; + } }); +/** + * 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 */ @@ -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); diff --git a/chrome-extension/content.js b/chrome-extension/content.js index c236193..fb098ca 100644 --- a/chrome-extension/content.js +++ b/chrome-extension/content.js @@ -11,14 +11,48 @@ apiUrl: 'http://localhost:5000/api', enabled: true, batchSize: 50, - extractInterval: 5000, // 5 seconds + extractInterval: 5000, // milliseconds + apiKey: '', }; + function normalizeInterval(value, fallback) { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric <= 0) { + return fallback; + } + return numeric >= 1000 ? numeric : numeric * 1000; + } + + function applyConfigUpdate(update = {}) { + if (!update || typeof update !== 'object') { + return; + } + + if (typeof update.apiUrl === 'string' && update.apiUrl.trim()) { + config.apiUrl = update.apiUrl.trim(); + } + if (typeof update.enabled === 'boolean') { + config.enabled = update.enabled; + } + if (update.batchSize !== undefined) { + const numeric = Number(update.batchSize); + if (Number.isFinite(numeric) && numeric > 0) { + config.batchSize = numeric; + } + } + if (update.extractInterval !== undefined) { + config.extractInterval = normalizeInterval(update.extractInterval, config.extractInterval); + } + if (typeof update.apiKey === 'string') { + config.apiKey = update.apiKey; + } + } + // Load config from storage - chrome.storage.sync.get(['apiUrl', 'enabled'], (result) => { - if (result.apiUrl) config.apiUrl = result.apiUrl; - if (result.enabled !== undefined) config.enabled = result.enabled; - }); + chrome.storage.sync.get( + ['apiUrl', 'enabled', 'batchSize', 'extractInterval', 'apiKey'], + (result) => applyConfigUpdate(result) + ); // Message queue let messageQueue = []; @@ -79,6 +113,61 @@ ], }; + /** + * Send a message to the background service worker without surfacing no-listener errors. + */ + function safeSendMessage(message) { + chrome.runtime.sendMessage(message, () => chrome.runtime.lastError); + } + + /** + * Ask the background service worker to deliver a batch to the backend. + */ + function dispatchBatchToBackground(batchPayload) { + return new Promise((resolve, reject) => { + const payload = { + ...batchPayload, + apiUrl: config.apiUrl, + apiKey: config.apiKey + }; + chrome.runtime.sendMessage( + { + type: 'SEND_BATCH', + payload: payload + }, + (response) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error('sendMessage failed:', lastError); + reject(new Error(lastError.message)); + return; + } + if (!response) { + const error = new Error('No response from background script. Is it running?'); + error.isNetworkError = true; + reject(error); + return; + } + if (!response.success) { + const error = new Error(response?.error || 'Unknown error sending batch'); + if (response?.status) { + error.status = response.status; + } + if (response?.details) { + error.details = response.details; + } + if (response?.network) { + error.isNetworkError = true; + } + reject(error); + return; + } + resolve(response.result); + } + ); + }); + } + /** * Query selector with multiple options */ @@ -340,57 +429,40 @@ } try { - const response = await fetch(`${config.apiUrl}/messages/batch`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - messages: transformedMessages, - extractionId: extractionId, - metadata: { - userAgent: navigator.userAgent, - teamsUrl: window.location.href, - timestamp: new Date().toISOString() - } - }), + const result = await dispatchBatchToBackground({ + messages: transformedMessages, + extractionId, + metadata: { + userAgent: navigator.userAgent, + teamsUrl: window.location.href, + timestamp: new Date().toISOString() + } }); - if (response.ok) { - const result = await response.json(); - console.log(`✅ Successfully sent ${batch.length} messages:`, result); - - // Update local counter - extractedMessagesCount += batch.length; - retryCount = 0; // Reset retry count on success - retryDelay = 1000; // Reset delay - - // Notify background script - chrome.runtime.sendMessage({ - type: 'MESSAGES_SENT', - count: batch.length, - inserted: result.inserted || batch.length, - duplicates: result.duplicates || 0, - totalExtracted: extractedMessagesCount - }); - - // Send remaining messages if any - if (messageQueue.length > 0) { - setTimeout(() => sendMessages(), 100); - } - } else { - const errorText = await response.text(); - console.error(`❌ Failed to send messages (${response.status}):`, response.statusText, errorText); + console.log(`✅ Successfully sent ${batch.length} messages:`, result); + + // Update local counter + extractedMessagesCount += batch.length; + retryCount = 0; // Reset retry count on success + retryDelay = 1000; // Reset delay + + // Notify background script + safeSendMessage({ + type: 'MESSAGES_SENT', + count: batch.length, + inserted: result?.inserted || batch.length, + duplicates: result?.duplicates || 0, + totalExtracted: extractedMessagesCount + }); - // Re-add to queue for retry - messageQueue.unshift(...batch); - handleSendFailure(batch, `HTTP ${response.status}: ${response.statusText}`); + // Send remaining messages if any + if (messageQueue.length > 0) { + setTimeout(() => sendMessages(), 100); } } catch (error) { - console.error('❌ Network error sending messages:', error); + console.error('❌ Failed to deliver messages:', error); - // Check if it's a CORS error - if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { + if (error.isNetworkError || /Failed to fetch|NetworkError/i.test(error.message)) { console.error(` 🔧 BACKEND CONNECTION ISSUE DETECTED! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -409,11 +481,15 @@ Possible causes: Current queue size: ${messageQueue.length + batch.length} messages waiting ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `); + } else if (error.details) { + console.error('Backend response details:', error.details); } // Re-add to queue for retry messageQueue.unshift(...batch); - handleSendFailure(batch, error.message); + const detailSnippet = error.details ? String(error.details).slice(0, 400) : null; + const detailMessage = detailSnippet ? `${error.message}: ${detailSnippet}` : error.message; + handleSendFailure(batch, detailMessage); } finally { isSending = false; } @@ -429,7 +505,7 @@ Current queue size: ${messageQueue.length + batch.length} messages waiting console.log(`⏰ Will retry in ${retryDelay / 1000} seconds (attempt ${retryCount}/${maxRetries})`); // Notify background about the error but with retry pending - chrome.runtime.sendMessage({ + safeSendMessage({ type: 'EXTRACTION_ERROR', error: errorDetails, retrying: true, @@ -449,7 +525,7 @@ Current queue size: ${messageQueue.length + batch.length} messages waiting console.error(`❌ Failed to send batch after ${maxRetries} retries. Giving up on ${batch.length} messages.`); // Notify background about permanent failure - chrome.runtime.sendMessage({ + safeSendMessage({ type: 'EXTRACTION_ERROR', error: `Failed after ${maxRetries} retries: ${errorDetails}`, retrying: false, @@ -531,7 +607,7 @@ Current queue size: ${messageQueue.length + batch.length} messages waiting apiUrl: config.apiUrl }); } else if (message.type === 'UPDATE_CONFIG') { - config = { ...config, ...message.config }; + applyConfigUpdate(message.config); sendResponse({ success: true }); } else if (message.type === 'FORCE_FLUSH') { // Force send all queued messages diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 88d2fcc..14590b7 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Teams Message Extractor", - "version": "1.0.0", + "version": "1.0.1", "description": "Extract and monitor Microsoft Teams messages for Claude Desktop integration", "permissions": [ "storage", diff --git a/chrome-extension/options.html b/chrome-extension/options.html index 8257f48..82e287e 100644 --- a/chrome-extension/options.html +++ b/chrome-extension/options.html @@ -163,6 +163,18 @@

Advanced Options

+
+

Claude Desktop Integration

+

+ Install the Teams Extractor as a Model Context Protocol (MCP) extension for Claude Desktop. + This allows Claude to access your Teams messages directly. +

+
+ +
+
+
+
diff --git a/chrome-extension/options.js b/chrome-extension/options.js index d2c7520..e8d373a 100644 --- a/chrome-extension/options.js +++ b/chrome-extension/options.js @@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('saveBtn').addEventListener('click', saveSettings); document.getElementById('testBtn').addEventListener('click', testConnection); document.getElementById('resetBtn').addEventListener('click', resetSettings); + document.getElementById('installMcpBtn').addEventListener('click', installMcpExtension); }); /** @@ -72,7 +73,7 @@ function saveSettings() { chrome.tabs.sendMessage(tab.id, { type: 'UPDATE_CONFIG', config: settings - }); + }, () => chrome.runtime.lastError); }); }); }); @@ -128,3 +129,41 @@ function showMessage(message, type) { statusMessage.className = 'status-message'; }, 5000); } + +// MCP Installation +/** + * Install MCP extension for Claude Desktop + */ +async function installMcpExtension() { + const apiUrl = document.getElementById('apiUrl').value.trim(); + const mcpStatus = document.getElementById('mcpStatus'); + + if (!apiUrl) { + showMessage('Please save a valid API URL before installing.', 'error'); + return; + } + + mcpStatus.textContent = 'Installation in progress...'; + mcpStatus.className = 'status-message'; // Reset classes + + try { + const baseUrl = apiUrl.replace('/api', ''); + const response = await fetch(`${baseUrl}/api/mcp/install`, { + method: 'POST', + }); + + const result = await response.json(); + + if (response.ok) { + mcpStatus.textContent = `Installation successful: ${result.message}`; + mcpStatus.classList.add('success'); + } else { + mcpStatus.textContent = `Installation failed: ${result.error || 'Unknown error'}`; + mcpStatus.classList.add('error'); + } + } catch (error) { + console.error('Error installing MCP extension:', error); + mcpStatus.textContent = `Failed to connect to backend: ${error.message}`; + mcpStatus.classList.add('error'); + } +} diff --git a/claude-extension/README.md b/claude-extension/README.md new file mode 100644 index 0000000..fa7bffe --- /dev/null +++ b/claude-extension/README.md @@ -0,0 +1,28 @@ +# Teams Extractor MCP Claude Extension + +This directory contains the manifest and documentation used to package the Teams Extractor MCP server as a Claude Desktop extension. The generated package lets you install the MCP server from Claude Desktop → **Developer → Extensions → Install Extension** without editing configuration files by hand. + +## Building the Package + +Use the helper script from the repository root: + +```bash +bash scripts/build_claude_extension.sh +``` + +The script performs the following steps: + +1. Copies the Claude manifest and MCP server sources into `dist/claude-extension/teams-extractor-mcp/` +2. Installs production-only npm dependencies for the server +3. Archives everything into `dist/claude-extension/teams-extractor-mcp.zip` + +## Installing in Claude Desktop + +1. Run the build script above +2. Open Claude Desktop on your machine +3. Navigate to **Developer → Extensions → Install Extension** +4. Select the generated `teams-extractor-mcp.zip` +5. When prompted, provide the PostgreSQL connection string used by Teams Extractor +6. Restart Claude Desktop so the new MCP server is available + +After installation you should see the `teams-extractor-mcp` server listed with its tools in the Claude Desktop extension manager. diff --git a/claude-extension/manifest.json b/claude-extension/manifest.json new file mode 100644 index 0000000..8d3bd37 --- /dev/null +++ b/claude-extension/manifest.json @@ -0,0 +1,56 @@ +{ + "dxt_version": "0.1", + "name": "Teams Extractor MCP", + "version": "1.0.0", + "description": "Install the Teams Extractor MCP server in Claude Desktop to query Teams history through Model Context Protocol tools.", + "keywords": [ + "teams", + "mcp", + "claude", + "messages", + "postgresql" + ], + "author": { + "name": "Teams Extractor", + "url": "https://github.com/badtux/teams-extractor" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/badtux/teams-extractor" + }, + "homepage": "https://github.com/badtux/teams-extractor", + "server": { + "type": "node", + "entry_point": "mcp-server/index.js", + "mcp_config": { + "command": "node", + "args": [ + "${__dirname}/mcp-server/index.js" + ], + "env": { + "MCP_DATABASE_URL": "${user_config.database_url}" + } + } + }, + "user_config": { + "database_url": { + "type": "string", + "title": "Database URL", + "description": "PostgreSQL connection string for the Teams Extractor database (e.g. postgresql://user:pass@localhost:5432/teams_extractor).", + "required": true, + "sensitive": true + } + }, + "compatibility": { + "claude_desktop": ">=0.2.16", + "platforms": [ + "darwin", + "win32", + "linux" + ], + "runtimes": { + "node": ">=18.0.0" + } + } +} diff --git a/docs/ADMIN_GUIDE.md b/docs/ADMIN_GUIDE.md index 62e1dba..58d42da 100644 --- a/docs/ADMIN_GUIDE.md +++ b/docs/ADMIN_GUIDE.md @@ -210,6 +210,18 @@ export PROCESSOR_DATA_DIR=/custom/path/data - Set target channel: `Güncelleme Planlama` - Add trigger keywords: `Güncellendi`, `Yaygınlaştırıldı` +### Claude Desktop MCP Extension + +1. **Build package** + - From the repository root run `bash scripts/build_claude_extension.sh` + - The archive is created at `dist/claude-extension/teams-extractor-mcp.zip` +2. **Install in Claude** + - Open Claude Desktop → Developer → Extensions → Install Extension + - Select the generated ZIP file + - Provide the Teams Extractor PostgreSQL connection string when prompted + - Ensure "Use Built-in Node.js for MCP" remains enabled (default on macOS) + - Restart Claude Desktop to load the new MCP server + ## Deployment ### Production Deployment with Docker diff --git a/mcp-server/README.md b/mcp-server/README.md index 6d3eeee..27202d7 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -23,9 +23,22 @@ Model Context Protocol (MCP) server for querying Teams messages directly from Cl ## 🛠️ Installation on Mac -### Quick Setup (Recommended) +### Install via Claude Desktop Extension UI (Recommended) -This is the easiest way to install the MCP server for Claude Desktop on your Mac: +1. Build the Claude extension package from the repository root: + ```bash + bash scripts/build_claude_extension.sh + ``` + The script copies the MCP server, installs production dependencies, and produces `dist/claude-extension/teams-extractor-mcp.zip`. +2. Open Claude Desktop and navigate to **Developer → Extensions → Install Extension** +3. Choose the generated `teams-extractor-mcp.zip` +4. When prompted, paste your PostgreSQL connection string (e.g. `postgresql://user:password@localhost:5432/teams_extractor`) +5. Ensure **Use Built-in Node.js for MCP** is enabled in the Claude settings (the default on macOS) +6. Restart Claude Desktop — the `teams-extractor-mcp` server and its tools will be available immediately + +### Command Line Setup (macOS) + +If you prefer a script that edits Claude's configuration file directly: ```bash cd mcp-server diff --git a/scripts/build_claude_extension.sh b/scripts/build_claude_extension.sh new file mode 100755 index 0000000..3fde05f --- /dev/null +++ b/scripts/build_claude_extension.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BUILD_ROOT="$ROOT_DIR/dist/claude-extension" +EXTENSION_NAME="teams-extractor-mcp" +EXTENSION_DIR="$BUILD_ROOT/$EXTENSION_NAME" + +echo "==> Preparing build directories" +rm -rf "$BUILD_ROOT" +mkdir -p "$EXTENSION_DIR" + +echo "==> Copying Claude extension manifest and docs" +cp "$ROOT_DIR/claude-extension/manifest.json" "$EXTENSION_DIR/manifest.json" +cp "$ROOT_DIR/claude-extension/README.md" "$EXTENSION_DIR/README.md" + +echo "==> Copying MCP server sources" +rsync -a --exclude node_modules "$ROOT_DIR/mcp-server/" "$EXTENSION_DIR/mcp-server/" + +echo "==> Installing production dependencies" +(cd "$EXTENSION_DIR/mcp-server" && npm ci --omit=dev) + +echo "==> Packaging Claude extension" +(cd "$BUILD_ROOT" && zip -r "$EXTENSION_NAME.zip" "$EXTENSION_NAME" >/dev/null) + +echo "" +echo "✅ Claude extension packaged:" +echo " $BUILD_ROOT/$EXTENSION_NAME.zip" +echo "" +echo "Install it from Claude Desktop → Developer → Extensions → Install Extension."