This is page 2 of 3. Use http://codebase.md/mohalmah/google-appscript-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .env.example
├── .gitattributes
├── .gitignore
├── .gitkeep
├── commands
│ └── tools.js
├── demo
│ └── google app script mcp demo.gif
├── Dockerfile
├── ERROR_HANDLING_ENHANCEMENT.md
├── helpers
│ ├── check-deployments.js
│ ├── check-head-deployment.js
│ ├── convert-to-oauth.js
│ ├── create-proper-webapp.js
│ ├── create-web-app-fixed.js
│ ├── create-web-app.js
│ └── create-webapp-deployment.js
├── index.js
├── lib
│ ├── authHelper.js
│ ├── logger.js
│ ├── oauth-helper.js
│ ├── tokenManager.js
│ └── tools.js
├── LOGGING.md
├── mcpServer.js
├── OAUTH_IMPLEMENTATION.md
├── OAUTH_SETUP.md
├── oauth-setup.js
├── package-lock.json
├── package.json
├── README.md
├── test
│ ├── debug-content-fetch.js
│ ├── debug-deployment.js
│ ├── debug-mcp-deployment.js
│ ├── debug-test.js
│ ├── deploy-complete-webapp.js
│ ├── oauth-setup-broken.js
│ ├── oauth-setup-fixed.js
│ ├── simple-oauth-test.js
│ ├── simple-test.js
│ ├── test-complete-mcp-webapp.js
│ ├── test-fields-issue.js
│ ├── test-logging.js
│ ├── test-mcp-content.js
│ ├── test-mcp-deployment-direct.js
│ ├── test-mcp-deployment-fix.js
│ ├── test-mcp-deployment-get.js
│ ├── test-mcp-errors.js
│ ├── test-mcp-fetch-processes.js
│ ├── test-mcp-processes.js
│ ├── test-mcp-tools.js
│ ├── test-mcp-version-fix.js
│ ├── test-oauth.js
│ ├── test-token-management.js
│ ├── test-versions-list.js
│ ├── update-and-deploy-dark-theme.js
│ ├── update-error-handling.js
│ ├── update-tools.js
│ └── update-webapp-deployment.js
└── tools
├── google-app-script-api
│ └── apps-script-api
│ ├── script-processes-list-script-processes.js
│ ├── script-processes-list.js
│ ├── script-projects-create.js
│ ├── script-projects-deployments-create.js
│ ├── script-projects-deployments-delete.js
│ ├── script-projects-deployments-get.js
│ ├── script-projects-deployments-list.js
│ ├── script-projects-deployments-update.js
│ ├── script-projects-get-content.js
│ ├── script-projects-get-metrics.js
│ ├── script-projects-get.js
│ ├── script-projects-update-content.js
│ ├── script-projects-versions-create.js
│ ├── script-projects-versions-get.js
│ ├── script-projects-versions-list.js
│ └── script-scripts-run.js
└── paths.js
```
# Files
--------------------------------------------------------------------------------
/helpers/convert-to-oauth.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 | import { fileURLToPath } from 'url';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | // Files to update
11 | const filesToUpdate = [
12 | 'tools/google-app-script-api/apps-script-api/script-projects-deployments-create.js',
13 | 'tools/google-app-script-api/apps-script-api/script-projects-deployments-get.js',
14 | 'tools/google-app-script-api/apps-script-api/script-projects-deployments-list.js',
15 | 'tools/google-app-script-api/apps-script-api/script-projects-deployments-update.js',
16 | 'tools/google-app-script-api/apps-script-api/script-projects-get-content.js',
17 | 'tools/google-app-script-api/apps-script-api/script-projects-get-metrics.js',
18 | 'tools/google-app-script-api/apps-script-api/script-projects-update-content.js',
19 | 'tools/google-app-script-api/apps-script-api/script-projects-versions-create.js',
20 | 'tools/google-app-script-api/apps-script-api/script-projects-versions-get.js',
21 | 'tools/google-app-script-api/apps-script-api/script-projects-versions-list.js',
22 | 'tools/google-app-script-api/apps-script-api/script-processes-list-script-processes.js'
23 | ];
24 |
25 | function updateFileForOAuth(filePath) {
26 | const fullPath = path.join(__dirname, filePath);
27 |
28 | if (!fs.existsSync(fullPath)) {
29 | console.log(`⚠️ File not found: ${filePath}`);
30 | return;
31 | }
32 |
33 | let content = fs.readFileSync(fullPath, 'utf8');
34 |
35 | // Check if already updated
36 | if (content.includes("import { getAuthHeaders } from '../../../lib/oauth-helper.js';")) {
37 | console.log(`✅ Already updated: ${filePath}`);
38 | return;
39 | }
40 |
41 | // Add import at the top
42 | if (!content.includes("import { getAuthHeaders }")) {
43 | content = `import { getAuthHeaders } from '../../../lib/oauth-helper.js';\n\n${content}`;
44 | }
45 |
46 | // Remove API key token assignment
47 | content = content.replace(/\s*const token = process\.env\.GOOGLE_APP_SCRIPT_API_API_KEY;\s*/g, '');
48 | content = content.replace(/\s*const apiKey = process\.env\.GOOGLE_APP_SCRIPT_API_API_KEY;\s*/g, '');
49 |
50 | // Replace header setup
51 | const headerPatterns = [
52 | // Pattern 1: Simple headers with token check
53 | /(\s*)\/\/ Set up headers for the request\s*\n\s*const headers = \{\s*\n\s*['"]Accept['"]:\s*['"]application\/json['"],?\s*\n\s*\};\s*\n\s*\/\/ If a token is provided, add it to the Authorization header\s*\n\s*if \(token\) \{\s*\n\s*headers\[['"]Authorization['"]\] = `Bearer \$\{token\}`;?\s*\n\s*\}/g,
54 |
55 | // Pattern 2: Headers with Content-Type
56 | /(\s*)\/\/ Set up headers for the request\s*\n\s*const headers = \{\s*\n\s*['"]Content-Type['"]:\s*['"]application\/json['"],?\s*\n\s*['"]Accept['"]:\s*['"]application\/json['"],?\s*\n\s*\};\s*\n\s*\/\/ If a token is provided, add it to the Authorization header\s*\n\s*if \(token\) \{\s*\n\s*headers\[['"]Authorization['"]\] = `Bearer \$\{token\}`;?\s*\n\s*\}/g,
57 |
58 | // Pattern 3: Authorization-only headers
59 | /(\s*)\/\/ Set up headers for the request\s*\n\s*const headers = \{\s*\n\s*['"]Authorization['"]:\s*`Bearer \$\{token\}`,?\s*\n\s*['"]Accept['"]:\s*['"]application\/json['"],?\s*\n\s*\}/g
60 | ];
61 |
62 | let headerReplaced = false;
63 | headerPatterns.forEach(pattern => {
64 | if (pattern.test(content) && !headerReplaced) {
65 | content = content.replace(pattern, '$1// Get OAuth headers\n$1const headers = await getAuthHeaders();');
66 | headerReplaced = true;
67 | }
68 | });
69 |
70 | // If no pattern matched, try a simpler approach
71 | if (!headerReplaced) {
72 | // Replace any Authorization header assignment
73 | content = content.replace(/headers\[['"]Authorization['"]\]\s*=\s*`Bearer \$\{token\}`;?/g, '');
74 |
75 | // Add OAuth headers if we can find where headers are defined
76 | if (content.includes('const headers = {') && !content.includes('await getAuthHeaders()')) {
77 | content = content.replace(
78 | /(const headers = \{[^}]*\});/,
79 | 'const headers = await getAuthHeaders();'
80 | );
81 | }
82 | }
83 |
84 | // Wrap the fetch request in try-catch if not already
85 | if (!content.includes('try {') && content.includes('const response = await fetch')) {
86 | content = content.replace(
87 | /(const executeFunction = async \([^}]*\) => \{)/,
88 | '$1\n try {'
89 | );
90 | content = content.replace(
91 | /(return data;\s*\}\s*catch)/,
92 | '$1'
93 | );
94 | }
95 |
96 | // Write the updated content
97 | fs.writeFileSync(fullPath, content, 'utf8');
98 | console.log(`✅ Updated: ${filePath}`);
99 | }
100 |
101 | // Update all files
102 | console.log('🔄 Converting Google Apps Script API files to use OAuth authentication...\n');
103 |
104 | filesToUpdate.forEach(updateFileForOAuth);
105 |
106 | console.log('\n✨ OAuth conversion completed!');
107 | console.log('\n📝 Next steps:');
108 | console.log('1. Update your .env file with OAuth credentials');
109 | console.log('2. Follow the OAUTH_SETUP.md guide to get your credentials');
110 | console.log('3. Test the authentication with one of the tools');
111 |
```
--------------------------------------------------------------------------------
/OAUTH_IMPLEMENTATION.md:
--------------------------------------------------------------------------------
```markdown
1 | # Enhanced OAuth Implementation Summary
2 |
3 | ## What We've Implemented
4 |
5 | ### 1. **Complete OAuth2 Flow** (`lib/oauth-helper.js`)
6 | - ✅ **Automatic browser opening** for OAuth authorization
7 | - ✅ **Local callback server** to handle OAuth responses
8 | - ✅ **Token management** with automatic refresh
9 | - ✅ **Fallback mechanisms** when refresh tokens expire
10 | - ✅ **Detailed logging** throughout the process
11 | - ✅ **Error handling** with specific troubleshooting guidance
12 |
13 | ### 2. **Enhanced Test Script** (`test-oauth.js`)
14 | - ✅ **Comprehensive logging** with timestamps and performance metrics
15 | - ✅ **Detailed error information** including stack traces
16 | - ✅ **Environment validation** checking for .env file and credentials
17 | - ✅ **System information** logging for debugging
18 | - ✅ **OAuth credential verification** (without exposing sensitive data)
19 |
20 | ### 3. **OAuth Setup Script** (`oauth-setup.js`)
21 | - ✅ **Interactive OAuth flow** to obtain refresh tokens
22 | - ✅ **Automatic .env file updates** with new refresh tokens
23 | - ✅ **Credential validation** before starting the flow
24 | - ✅ **User-friendly web interface** for OAuth completion
25 | - ✅ **Comprehensive error handling** and troubleshooting
26 |
27 | ### 4. **Updated Configuration**
28 | - ✅ **Package.json scripts** for easy access
29 | - ✅ **Environment variables** properly configured
30 | - ✅ **Dependencies installed** (googleapis, open)
31 | - ✅ **Comprehensive setup guide** (OAUTH_SETUP.md)
32 |
33 | ## Key Features Similar to Your Working App
34 |
35 | ### 🔐 **OAuth2 Client Creation**
36 | ```javascript
37 | const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
38 | ```
39 |
40 | ### 🌐 **Local Server for Callback**
41 | ```javascript
42 | const server = createServer(async (req, res) => {
43 | // Handle OAuth callback with detailed logging
44 | });
45 | ```
46 |
47 | ### 🔄 **Token Exchange & Storage**
48 | ```javascript
49 | const { tokens: newTokens } = await oAuth2Client.getToken(code);
50 | tokens = newTokens;
51 | ```
52 |
53 | ### 📱 **Browser Automation**
54 | ```javascript
55 | open(authUrl).catch(err => {
56 | console.error('❌ Failed to open browser:', err);
57 | console.log('🔗 Please manually open this URL in your browser:', authUrl);
58 | });
59 | ```
60 |
61 | ### 🔄 **Automatic Token Refresh**
62 | ```javascript
63 | if (refreshToken && refreshToken !== 'your_refresh_token_here') {
64 | // Use refresh token to get new access token
65 | }
66 | ```
67 |
68 | ## How to Use
69 |
70 | ### 1. **First Time Setup**
71 | ```bash
72 | npm run setup-oauth
73 | ```
74 | - Opens browser automatically
75 | - Handles OAuth flow
76 | - Updates .env file with refresh token
77 |
78 | ### 2. **Test OAuth Setup**
79 | ```bash
80 | npm run test-oauth
81 | ```
82 | - Validates credentials
83 | - Tests token retrieval
84 | - Shows detailed diagnostic information
85 |
86 | ### 3. **Use in Your MCP Tools**
87 | The OAuth helper will now:
88 | - ✅ Use refresh token if available
89 | - ✅ Start interactive OAuth flow if needed
90 | - ✅ Handle token expiration automatically
91 | - ✅ Provide detailed error information
92 |
93 | ## Enhanced Error Logging
94 |
95 | ### **Success Path Logging:**
96 | - ⏰ Timestamps for all operations
97 | - 📊 Performance metrics
98 | - 🔑 Token information (safely masked)
99 | - 📋 Request/response details
100 |
101 | ### **Error Path Logging:**
102 | - 🕐 Error timestamps
103 | - 📋 Complete error details (message, stack, status codes)
104 | - 🔍 Environment diagnostics
105 | - 📂 File system checks
106 | - 🔧 Comprehensive troubleshooting steps
107 |
108 | ### **Example Enhanced Error Output:**
109 | ```
110 | ❌ OAuth authentication failed!
111 | 🕐 Error occurred at: 2025-05-31T10:30:45.123Z
112 |
113 | 📋 Error Details:
114 | 📄 Message: Failed to refresh token: invalid_grant
115 | 🏷️ Name: Error
116 | 📊 Stack trace:
117 | Error: Failed to refresh token: invalid_grant
118 | at getOAuthAccessToken (file:///oauth-helper.js:245:13)
119 | at testOAuthAuthentication (file:///test-oauth.js:28:31)
120 |
121 | 🔍 Environment Check:
122 | 📂 Current directory: c:\Users\mohal\Downloads\postman-mcp-server
123 | 🔧 Node.js version: v18.17.0
124 | 📄 .env file exists: true
125 | 🔑 GOOGLE_CLIENT_ID present: true
126 | 🔑 GOOGLE_CLIENT_SECRET present: true
127 | 🔑 GOOGLE_REFRESH_TOKEN present: true
128 |
129 | 🔧 Troubleshooting steps:
130 | 1. Check that your .env file contains valid OAuth credentials
131 | 2. Verify your client ID and client secret are correct
132 | 3. Ensure your refresh token is valid and not expired
133 | 4. Follow the OAUTH_SETUP.md guide to obtain new credentials if needed
134 | 5. Make sure the Google Apps Script API is enabled in your GCP project
135 | 6. Check your internet connection and firewall settings
136 | 7. Verify that the oauth-helper.js file exists and is accessible
137 | ```
138 |
139 | ## Security Features
140 |
141 | - 🔐 **No credentials exposed** in logs
142 | - 🔑 **Secure token storage** in environment variables
143 | - 🌐 **Local-only callback server** (port 3001)
144 | - ⏱️ **Automatic server timeout** (5 minutes)
145 | - 🚪 **Clean token cleanup** on logout
146 |
147 | ## Next Steps
148 |
149 | 1. **Run the setup**: `npm run setup-oauth`
150 | 2. **Test the implementation**: `npm run test-oauth`
151 | 3. **Use your MCP tools** with confidence!
152 |
153 | The OAuth implementation now matches the robustness and user experience of your working Express app while providing enhanced debugging capabilities for easier troubleshooting.
154 |
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-projects-deployments-list.js:
--------------------------------------------------------------------------------
```javascript
1 | import { getOAuthAccessToken } from '../../../lib/oauth-helper.js';
2 | import { logger } from '../../../lib/logger.js';
3 |
4 | /**
5 | * Function to list the deployments of a Google Apps Script project.
6 | *
7 | * @param {Object} args - Arguments for the deployment listing.
8 | * @param {string} args.scriptId - The ID of the script project.
9 | * @param {number} [args.pageSize=50] - The number of deployments to return per page.
10 | * @param {string} [args.pageToken] - Token for pagination.
11 | * @param {string} [args.fields] - Selector specifying which fields to include in a partial response.
12 | * @param {boolean} [args.prettyPrint=true] - Returns response with indentations and line breaks.
13 | * @returns {Promise<Object>} - The result of the deployments listing.
14 | */
15 | const executeFunction = async ({ scriptId, pageSize = 50, pageToken, fields, prettyPrint = true }) => {
16 | const baseUrl = 'https://script.googleapis.com';
17 | const startTime = Date.now();
18 |
19 | logger.info('API_CALL', 'Starting deployments list request', {
20 | scriptId,
21 | pageSize,
22 | pageToken: pageToken ? 'provided' : 'none',
23 | fields: fields || 'all',
24 | baseUrl
25 | });
26 |
27 | try {
28 | // Get OAuth access token
29 | logger.debug('API_CALL', 'Getting OAuth access token');
30 | const token = await getOAuthAccessToken();
31 | logger.debug('API_CALL', 'OAuth token obtained successfully');
32 |
33 | // Construct the URL with query parameters
34 | const url = new URL(`${baseUrl}/v1/projects/${scriptId}/deployments`);
35 | url.searchParams.append('pageSize', pageSize.toString());
36 | if (pageToken) url.searchParams.append('pageToken', pageToken);
37 | if (fields) url.searchParams.append('fields', fields);
38 | url.searchParams.append('alt', 'json');
39 | url.searchParams.append('prettyPrint', prettyPrint.toString());
40 |
41 | logger.debug('API_CALL', 'Constructed API URL', {
42 | url: url.toString(),
43 | pathSegments: url.pathname.split('/'),
44 | queryParams: Object.fromEntries(url.searchParams)
45 | });
46 |
47 | // Set up headers for the request
48 | const headers = {
49 | 'Accept': 'application/json',
50 | 'Authorization': `Bearer ${token}`
51 | };
52 |
53 | logger.logAPICall('GET', url.toString(), headers);
54 |
55 | // Perform the fetch request
56 | const fetchStartTime = Date.now();
57 | const response = await fetch(url.toString(), {
58 | method: 'GET',
59 | headers
60 | });
61 |
62 | const fetchDuration = Date.now() - fetchStartTime;
63 | const responseSize = response.headers.get('content-length') || 'unknown';
64 |
65 | logger.logAPIResponse('GET', url.toString(), response.status, fetchDuration, responseSize);
66 |
67 | // Check if the response was successful
68 | if (!response.ok) {
69 | const errorText = await response.text();
70 |
71 | logger.error('API_CALL', 'API request failed', {
72 | status: response.status,
73 | statusText: response.statusText,
74 | url: url.toString(),
75 | errorResponse: errorText,
76 | duration: Date.now() - startTime
77 | });
78 |
79 | console.error('API Error Response:', errorText);
80 | throw new Error(`HTTP ${response.status}: ${errorText}`);
81 | }
82 |
83 | // Parse and return the response data
84 | const data = await response.json();
85 | const totalDuration = Date.now() - startTime;
86 |
87 | logger.info('API_CALL', 'Deployments list request completed successfully', {
88 | scriptId,
89 | deploymentCount: data.deployments ? data.deployments.length : 0,
90 | hasNextPageToken: !!data.nextPageToken,
91 | totalDuration: `${totalDuration}ms`,
92 | responseSize: JSON.stringify(data).length
93 | });
94 |
95 | return data;
96 | } catch (error) {
97 | const totalDuration = Date.now() - startTime;
98 |
99 | logger.error('API_CALL', 'Deployments list request failed', {
100 | scriptId,
101 | error: {
102 | message: error.message,
103 | stack: error.stack
104 | },
105 | totalDuration: `${totalDuration}ms`
106 | });
107 |
108 | console.error('Error listing deployments:', error);
109 | return {
110 | error: 'An error occurred while listing deployments.',
111 | details: {
112 | message: error.message,
113 | scriptId,
114 | timestamp: new Date().toISOString()
115 | }
116 | };
117 | }
118 | };
119 |
120 | /**
121 | * Tool configuration for listing deployments of a Google Apps Script project.
122 | * @type {Object}
123 | */
124 | const apiTool = {
125 | function: executeFunction,
126 | definition: {
127 | type: 'function',
128 | function: {
129 | name: 'script_projects_deployments_list',
130 | description: 'Lists the deployments of an Apps Script project.',
131 | parameters: {
132 | type: 'object',
133 | properties: {
134 | scriptId: {
135 | type: 'string',
136 | description: 'The ID of the script project.'
137 | },
138 | pageSize: {
139 | type: 'integer',
140 | description: 'The number of deployments to return per page.'
141 | },
142 | pageToken: {
143 | type: 'string',
144 | description: 'Token for pagination.'
145 | },
146 | fields: {
147 | type: 'string',
148 | description: 'Selector specifying which fields to include in a partial response.'
149 | },
150 | prettyPrint: {
151 | type: 'boolean',
152 | description: 'Returns response with indentations and line breaks.'
153 | }
154 | },
155 | required: ['scriptId']
156 | }
157 | }
158 | }
159 | };
160 |
161 | export { apiTool };
```
--------------------------------------------------------------------------------
/test/update-tools.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Script to automatically update all Google Apps Script API tools
5 | * to use automatic OAuth and enhanced error handling
6 | */
7 |
8 | import { readFileSync, writeFileSync, readdirSync } from 'fs';
9 | import { join } from 'path';
10 |
11 | const toolsDir = 'tools/google-app-script-api/apps-script-api';
12 |
13 | // Enhanced error handling template
14 | const errorHandlingTemplate = `
15 | } catch (error) {
16 | console.error('❌ Error in API call:', {
17 | message: error.message,
18 | stack: error.stack,
19 | timestamp: new Date().toISOString()
20 | });
21 |
22 | // Return detailed error information for debugging
23 | return {
24 | error: true,
25 | message: error.message,
26 | details: {
27 | timestamp: new Date().toISOString(),
28 | errorType: error.name || 'Unknown'
29 | }
30 | };
31 | }
32 | `;
33 |
34 | // Function to remove OAuth parameters from tool definitions
35 | function updateToolDefinition(content) {
36 | // Remove OAuth-related parameters from the function signature
37 | content = content.replace(
38 | /(\{[^}]*?)(key|access_token|oauth_token)[\s\S]*?(,\s*)?/g,
39 | (match, start, param, comma) => {
40 | // If this is the last parameter before closing brace, remove the comma
41 | if (match.includes('}')) {
42 | return start.replace(/,\s*$/, '');
43 | }
44 | return start;
45 | }
46 | );
47 |
48 | // Remove OAuth parameters from URL construction
49 | content = content.replace(/\s*url\.searchParams\.append\([^)]*(?:key|access_token|oauth_token)[^)]*\);?\n?/g, '');
50 |
51 | // Update parameter validation to only require non-OAuth params
52 | content = content.replace(
53 | /required:\s*\[[^\]]*(?:key|access_token|oauth_token)[^\]]*\]/g,
54 | (match) => {
55 | // Extract non-OAuth required parameters
56 | const params = match.match(/'([^']+)'/g) || [];
57 | const nonOAuthParams = params.filter(p =>
58 | !p.includes('key') &&
59 | !p.includes('access_token') &&
60 | !p.includes('oauth_token')
61 | );
62 | return `required: [${nonOAuthParams.join(', ')}]`;
63 | }
64 | );
65 |
66 | // Remove OAuth parameter definitions from schema
67 | content = content.replace(/\s*(?:key|access_token|oauth_token):\s*\{[^}]*\},?\n?/g, '');
68 |
69 | // Add OAuth auto-handling note to description
70 | content = content.replace(
71 | /(description:\s*')([^']*?)(')/g,
72 | '$1$2 OAuth authentication is handled automatically.$3'
73 | );
74 |
75 | return content;
76 | }
77 |
78 | // Function to enhance error handling
79 | function enhanceErrorHandling(content) {
80 | // Replace simple error handling with detailed version
81 | content = content.replace(
82 | /if\s*\(\s*!response\.ok\s*\)\s*\{[\s\S]*?throw new Error\([^)]*\);?\s*\}/g,
83 | `if (!response.ok) {
84 | const errorText = await response.text();
85 | let errorData;
86 |
87 | try {
88 | errorData = JSON.parse(errorText);
89 | } catch (parseError) {
90 | errorData = { message: errorText };
91 | }
92 |
93 | const detailedError = {
94 | status: response.status,
95 | statusText: response.statusText,
96 | url: url.toString(),
97 | error: errorData,
98 | timestamp: new Date().toISOString()
99 | };
100 |
101 | console.error('❌ API Error Details:', JSON.stringify(detailedError, null, 2));
102 |
103 | throw new Error(\`API Error (\${response.status}): \${errorData.error?.message || errorData.message || 'Unknown error'}\`);
104 | }`
105 | );
106 |
107 | // Replace simple catch blocks with detailed error handling
108 | content = content.replace(
109 | /catch\s*\([^)]*\)\s*\{[\s\S]*?return\s*\{[^}]*error[^}]*\};?\s*\}/g,
110 | errorHandlingTemplate.trim()
111 | );
112 |
113 | return content;
114 | }
115 |
116 | // Get all tool files
117 | const toolFiles = readdirSync(toolsDir).filter(file => file.endsWith('.js'));
118 |
119 | console.log('🔧 Updating Google Apps Script API tools...');
120 | console.log(`📁 Found ${toolFiles.length} tool files to update`);
121 |
122 | let updatedCount = 0;
123 |
124 | for (const file of toolFiles) {
125 | try {
126 | const filePath = join(toolsDir, file);
127 | console.log(`🔄 Processing: ${file}`);
128 |
129 | let content = readFileSync(filePath, 'utf8');
130 |
131 | // Skip if already updated (check for OAuth auto-handling comment)
132 | if (content.includes('OAuth authentication is handled automatically')) {
133 | console.log(`✅ ${file} already updated, skipping`);
134 | continue;
135 | }
136 |
137 | // Apply updates
138 | content = updateToolDefinition(content);
139 | content = enhanceErrorHandling(content);
140 |
141 | // Add logging for API calls
142 | if (!content.includes('console.log') && content.includes('fetch(')) {
143 | content = content.replace(
144 | /(const url = new URL\([^;]+;)/,
145 | '$1\n console.log(\'🌐 API URL:\', url.toString());'
146 | );
147 |
148 | content = content.replace(
149 | /(const headers = await getAuthHeaders\(\);)/,
150 | '$1\n console.log(\'🔐 Authorization headers obtained successfully\');'
151 | );
152 |
153 | content = content.replace(
154 | /(const response = await fetch\([^;]+;)/,
155 | '$1\n console.log(\'📡 API Response Status:\', response.status, response.statusText);'
156 | );
157 | }
158 |
159 | writeFileSync(filePath, content);
160 | updatedCount++;
161 | console.log(`✅ Updated: ${file}`);
162 |
163 | } catch (error) {
164 | console.error(`❌ Error updating ${file}:`, error.message);
165 | }
166 | }
167 |
168 | console.log(`\n🎉 Successfully updated ${updatedCount} out of ${toolFiles.length} files`);
169 | console.log('✅ All tools now use automatic OAuth authentication and enhanced error handling');
170 |
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-projects-deployments-update.js:
--------------------------------------------------------------------------------
```javascript
1 | import { logger } from '../../../lib/logger.js';
2 |
3 | /**
4 | * Function to update a deployment of an Apps Script project.
5 | *
6 | * @param {Object} args - Arguments for the update.
7 | * @param {string} args.scriptId - The ID of the script to update.
8 | * @param {string} args.deploymentId - The ID of the deployment to update.
9 | * @param {Object} args.deploymentConfig - The configuration for the deployment.
10 | * @param {string} args.deploymentConfig.manifestFileName - The name of the manifest file.
11 | * @param {number} args.deploymentConfig.versionNumber - The version number of the deployment.
12 | * @param {string} args.deploymentConfig.description - A description of the deployment.
13 | * @returns {Promise<Object>} - The result of the deployment update.
14 | */
15 | const executeFunction = async ({ scriptId, deploymentId, deploymentConfig }) => {
16 | const baseUrl = 'https://script.googleapis.com';
17 | const token = process.env.GOOGLE_APP_SCRIPT_API_API_KEY;
18 | const apiKey = process.env.GOOGLE_APP_SCRIPT_API_API_KEY;
19 | const startTime = Date.now();
20 |
21 | try {
22 | logger.info('DEPLOYMENT_UPDATE', 'Starting deployment update', {
23 | scriptId,
24 | deploymentId,
25 | versionNumber: deploymentConfig?.versionNumber
26 | });
27 |
28 | // Construct the URL for the request
29 | const url = `${baseUrl}/v1/projects/${scriptId}/deployments/${deploymentId}?key=${apiKey}&prettyPrint=true`;
30 |
31 | // Set up headers for the request
32 | const headers = {
33 | 'Content-Type': 'application/json',
34 | 'Accept': 'application/json',
35 | 'Authorization': `Bearer ${token}`
36 | };
37 |
38 | // Prepare the body of the request
39 | const requestBody = { deploymentConfig };
40 | const body = JSON.stringify(requestBody);
41 |
42 | logger.logAPICall('PUT', url, headers, requestBody);
43 |
44 | // Perform the fetch request
45 | const fetchStartTime = Date.now();
46 | const response = await fetch(url, {
47 | method: 'PUT',
48 | headers,
49 | body
50 | });
51 |
52 | const fetchDuration = Date.now() - fetchStartTime;
53 | const responseSize = response.headers.get('content-length') || 'unknown';
54 |
55 | logger.logAPIResponse('PUT', url, response.status, fetchDuration, responseSize);
56 |
57 | // Check if the response was successful
58 | if (!response.ok) {
59 | const errorText = await response.text();
60 | let errorData;
61 |
62 | try {
63 | errorData = JSON.parse(errorText);
64 | } catch (parseError) {
65 | errorData = { message: errorText };
66 | }
67 |
68 | const detailedError = {
69 | status: response.status,
70 | statusText: response.statusText,
71 | url,
72 | errorResponse: errorData,
73 | duration: Date.now() - startTime,
74 | scriptId,
75 | deploymentId,
76 | timestamp: new Date().toISOString()
77 | };
78 |
79 | logger.error('DEPLOYMENT_UPDATE', 'API request failed', detailedError);
80 |
81 | console.error('❌ API Error Details:', JSON.stringify(detailedError, null, 2));
82 |
83 | throw new Error(`API Error (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
84 | }
85 |
86 | // Parse and return the response data
87 | const data = await response.json();
88 |
89 | logger.info('DEPLOYMENT_UPDATE', 'Successfully updated deployment', {
90 | scriptId,
91 | deploymentId,
92 | duration: Date.now() - startTime
93 | });
94 |
95 | console.log('✅ Successfully updated deployment');
96 | return data;
97 | } catch (error) {
98 | const errorDetails = {
99 | message: error.message,
100 | stack: error.stack,
101 | scriptId,
102 | deploymentId,
103 | duration: Date.now() - startTime,
104 | timestamp: new Date().toISOString(),
105 | errorType: error.name || 'Unknown'
106 | };
107 |
108 | logger.error('DEPLOYMENT_UPDATE', 'Error updating deployment', errorDetails);
109 |
110 | console.error('❌ Error updating deployment:', errorDetails);
111 |
112 | // Return detailed error information for debugging
113 | return {
114 | error: true,
115 | message: error.message,
116 | details: errorDetails,
117 | rawError: {
118 | name: error.name,
119 | stack: error.stack
120 | }
121 | };
122 | }
123 | };
124 |
125 | /**
126 | * Tool configuration for updating a deployment of an Apps Script project.
127 | * @type {Object}
128 | */
129 | const apiTool = {
130 | function: executeFunction,
131 | definition: {
132 | type: 'function',
133 | function: {
134 | name: 'script_projects_deployments_update',
135 | description: 'Updates a deployment of an Apps Script project.',
136 | parameters: {
137 | type: 'object',
138 | properties: {
139 | scriptId: {
140 | type: 'string',
141 | description: 'The ID of the script to update.'
142 | },
143 | deploymentId: {
144 | type: 'string',
145 | description: 'The ID of the deployment to update.'
146 | },
147 | deploymentConfig: {
148 | type: 'object',
149 | properties: {
150 | manifestFileName: {
151 | type: 'string',
152 | description: 'The name of the manifest file.'
153 | },
154 | versionNumber: {
155 | type: 'integer',
156 | description: 'The version number of the deployment.'
157 | },
158 | description: {
159 | type: 'string',
160 | description: 'A description of the deployment.'
161 | }
162 | },
163 | required: ['manifestFileName', 'versionNumber', 'description']
164 | }
165 | },
166 | required: ['scriptId', 'deploymentId', 'deploymentConfig']
167 | }
168 | }
169 | }
170 | };
171 |
172 | export { apiTool };
```
--------------------------------------------------------------------------------
/test/test-mcp-fetch-processes.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Simple MCP Server Test - Fetch Script Processes List
5 | * This test focuses on fetching script processes using the MCP tool to identify errors
6 | */
7 |
8 | import { discoverTools } from './lib/tools.js';
9 | import { logger } from './lib/logger.js';
10 |
11 | // Known working script ID from previous tests
12 | const SCRIPT_ID = '1fSY7y3Rh84FsgJmrFIMm4AUOV3mPgelLRvZ4Dahrv68zyDzX-cGbeYjn';
13 |
14 | async function testFetchProcesses() {
15 | console.log('🔄 Testing MCP Server - Fetch Script Processes List\n');
16 |
17 | try {
18 | // Set logging to capture detailed information
19 | process.env.LOG_LEVEL = 'debug';
20 |
21 | console.log('🔍 Step 1: Discovering MCP tools...');
22 | const tools = await discoverTools();
23 | console.log(`✅ Found ${tools.length} tools\n`);
24 |
25 | // Find the script processes list tool
26 | const processListTool = tools.find(tool =>
27 | tool.definition?.function?.name === 'd94_script_processes_list' ||
28 | tool.definition?.function?.name === 'script_processes_list'
29 | );
30 |
31 | if (!processListTool) {
32 | console.error('❌ Script processes list tool not found');
33 | console.log('Available tools:');
34 | tools.forEach(tool => {
35 | console.log(` - ${tool.definition?.function?.name}`);
36 | });
37 | return;
38 | }
39 |
40 | console.log('🎯 Step 2: Found script processes list tool');
41 | console.log(` Name: ${processListTool.definition.function.name}`);
42 | console.log(` Description: ${processListTool.definition.function.description}`);
43 | console.log(` Required params: ${processListTool.definition.function.parameters.required?.join(', ') || 'none'}\n`);
44 |
45 | // Test the tool with minimal parameters
46 | console.log('🚀 Step 3: Calling tool to fetch processes...');
47 | console.log(` Script ID: ${SCRIPT_ID}`);
48 |
49 | try {
50 | const startTime = Date.now();
51 |
52 | const result = await processListTool.function({
53 | scriptId: SCRIPT_ID,
54 | pageSize: 10
55 | });
56 |
57 | const duration = Date.now() - startTime;
58 |
59 | console.log(`✅ Success! Call completed in ${duration}ms`);
60 | console.log('\n📊 RESULT:');
61 | console.log('=' .repeat(50));
62 |
63 | if (result && typeof result === 'object') {
64 | if (result.processes && Array.isArray(result.processes)) {
65 | console.log(`Found ${result.processes.length} processes:`);
66 | result.processes.forEach((process, index) => {
67 | console.log(`\n Process ${index + 1}:`);
68 | console.log(` Function: ${process.functionName || 'N/A'}`);
69 | console.log(` Type: ${process.processType || 'N/A'}`);
70 | console.log(` Status: ${process.processStatus || 'N/A'}`);
71 | console.log(` Start: ${process.startTime || 'N/A'}`);
72 | console.log(` Duration: ${process.duration || 'N/A'}`);
73 | });
74 | } else {
75 | console.log('No processes found in result');
76 | }
77 |
78 | if (result.nextPageToken) {
79 | console.log(`\nNext page token available: ${result.nextPageToken.substring(0, 20)}...`);
80 | }
81 | } else {
82 | console.log('Unexpected result format:');
83 | console.log(JSON.stringify(result, null, 2));
84 | }
85 |
86 | } catch (error) {
87 | console.log('\n❌ ERROR OCCURRED:');
88 | console.log('=' .repeat(50));
89 | console.log(`Error Type: ${error.constructor.name}`);
90 | console.log(`Error Message: ${error.message}`);
91 |
92 | if (error.response) {
93 | console.log(`HTTP Status: ${error.response.status}`);
94 | console.log(`Response Data: ${JSON.stringify(error.response.data, null, 2)}`);
95 | }
96 |
97 | if (error.stack) {
98 | console.log('\nStack Trace:');
99 | console.log(error.stack);
100 | }
101 |
102 | // Log additional context if available
103 | if (error.config) {
104 | console.log('\nRequest Config:');
105 | console.log(`URL: ${error.config.url}`);
106 | console.log(`Method: ${error.config.method?.toUpperCase()}`);
107 | console.log(`Headers: ${JSON.stringify(error.config.headers, null, 2)}`);
108 | }
109 | }
110 |
111 | } catch (setupError) {
112 | console.log('\n💥 SETUP ERROR:');
113 | console.log('=' .repeat(50));
114 | console.log(`Error: ${setupError.message}`);
115 | console.log(`Stack: ${setupError.stack}`);
116 | }
117 |
118 | console.log('\n🏁 Test completed\n');
119 | }
120 |
121 | // Test with problematic fields parameter
122 | async function testProblematicFields() {
123 | console.log('🔥 Testing Problematic Fields Parameter\n');
124 |
125 | try {
126 | const tools = await discoverTools();
127 | const processListTool = tools.find(tool =>
128 | tool.definition?.function?.name === 'd94_script_processes_list' ||
129 | tool.definition?.function?.name === 'script_processes_list'
130 | );
131 |
132 | if (!processListTool) {
133 | console.log('❌ Tool not found for fields test');
134 | return;
135 | }
136 |
137 | console.log('🧪 Testing with known problematic fields parameter...');
138 |
139 | try {
140 | const result = await processListTool.function({
141 | scriptId: SCRIPT_ID,
142 | pageSize: 5,
143 | fields: 'processes(processType,functionName,startTime,duration,status)'
144 | });
145 |
146 | console.log('😮 Unexpected success with problematic fields!');
147 | console.log(JSON.stringify(result, null, 2));
148 |
149 | } catch (error) {
150 | console.log('✅ Expected error with invalid "status" field:');
151 | console.log(` Error: ${error.message}`);
152 |
153 | if (error.response?.data) {
154 | console.log(' API Response:');
155 | console.log(JSON.stringify(error.response.data, null, 2));
156 | }
157 | }
158 |
159 | } catch (error) {
160 | console.log(`❌ Fields test setup error: ${error.message}`);
161 | }
162 | }
163 |
164 | // Run both tests
165 | async function runAllTests() {
166 | await testFetchProcesses();
167 | console.log('\n' + '='.repeat(60) + '\n');
168 | await testProblematicFields();
169 | }
170 |
171 | runAllTests().catch(console.error);
172 |
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-projects-get-content.js:
--------------------------------------------------------------------------------
```javascript
1 | import { getOAuthAccessToken } from '../../../lib/oauth-helper.js';
2 | import { logger } from '../../../lib/logger.js';
3 |
4 | /**
5 | * Function to get the content of a Google Apps Script project.
6 | *
7 | * @param {Object} args - Arguments for the request.
8 | * @param {string} args.scriptId - The ID of the script project to retrieve content for.
9 | * @param {string} [args.versionNumber] - The version number of the script project.
10 | * @param {string} [args.fields] - Selector specifying which fields to include in a partial response.
11 | * @param {string} [args.alt="json"] - Data format for response.
12 | * @param {string} [args.key] - API key for the project.
13 | * @param {string} [args.access_token] - OAuth access token.
14 | * @param {string} [args.prettyPrint="true"] - Returns response with indentations and line breaks.
15 | * @returns {Promise<Object>} - The content of the script project.
16 | */
17 | const executeFunction = async ({ scriptId, versionNumber, fields, alt = "json", key, access_token, prettyPrint = "true" }) => {
18 | const baseUrl = 'https://script.googleapis.com';
19 | const startTime = Date.now();
20 |
21 | try {
22 | logger.info('SCRIPT_GET_CONTENT', 'Starting script content retrieval', { scriptId, versionNumber });
23 |
24 | // Get OAuth access token
25 | const token = await getOAuthAccessToken();
26 |
27 | // Construct the URL with query parameters
28 | const url = new URL(`${baseUrl}/v1/projects/${scriptId}/content`);
29 | if (versionNumber) url.searchParams.append('versionNumber', versionNumber);
30 | if (fields) url.searchParams.append('fields', fields);
31 | url.searchParams.append('alt', alt);
32 | if (key) url.searchParams.append('key', key);
33 | if (prettyPrint) url.searchParams.append('prettyPrint', prettyPrint);
34 |
35 | logger.debug('SCRIPT_GET_CONTENT', 'Constructed API URL', {
36 | url: url.toString(),
37 | pathSegments: url.pathname.split('/'),
38 | queryParams: Object.fromEntries(url.searchParams)
39 | });
40 |
41 | // Set up headers for the request
42 | const headers = {
43 | 'Accept': 'application/json',
44 | 'Authorization': `Bearer ${token}`
45 | };
46 |
47 | logger.logAPICall('GET', url.toString(), headers);
48 |
49 | // Perform the fetch request
50 | const fetchStartTime = Date.now();
51 | const response = await fetch(url.toString(), {
52 | method: 'GET',
53 | headers
54 | });
55 |
56 | const fetchDuration = Date.now() - fetchStartTime;
57 | const responseSize = response.headers.get('content-length') || 'unknown';
58 |
59 | logger.logAPIResponse('GET', url.toString(), response.status, fetchDuration, responseSize);
60 |
61 | // Check if the response was successful
62 | if (!response.ok) {
63 | const errorText = await response.text();
64 | let errorData;
65 |
66 | try {
67 | errorData = JSON.parse(errorText);
68 | } catch (parseError) {
69 | errorData = { message: errorText };
70 | }
71 |
72 | const detailedError = {
73 | status: response.status,
74 | statusText: response.statusText,
75 | url: url.toString(),
76 | errorResponse: errorData,
77 | duration: Date.now() - startTime,
78 | scriptId,
79 | versionNumber,
80 | timestamp: new Date().toISOString()
81 | };
82 |
83 | logger.error('SCRIPT_GET_CONTENT', 'API request failed', detailedError);
84 |
85 | console.error('❌ API Error Details:', JSON.stringify(detailedError, null, 2));
86 |
87 | throw new Error(`API Error (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
88 | }
89 |
90 | // Parse and return the response data
91 | const data = await response.json();
92 |
93 | logger.info('SCRIPT_GET_CONTENT', 'Successfully retrieved script content', {
94 | scriptId,
95 | versionNumber,
96 | filesCount: data.files?.length || 0,
97 | duration: Date.now() - startTime
98 | });
99 |
100 | console.log('✅ Successfully retrieved script content');
101 | return data;
102 | } catch (error) {
103 | const errorDetails = {
104 | message: error.message,
105 | stack: error.stack,
106 | scriptId,
107 | versionNumber,
108 | duration: Date.now() - startTime,
109 | timestamp: new Date().toISOString(),
110 | errorType: error.name || 'Unknown'
111 | };
112 |
113 | logger.error('SCRIPT_GET_CONTENT', 'Error getting script project content', errorDetails);
114 |
115 | console.error('❌ Error getting script project content:', errorDetails);
116 |
117 | // Return detailed error information for debugging
118 | return {
119 | error: true,
120 | message: error.message,
121 | details: errorDetails,
122 | rawError: {
123 | name: error.name,
124 | stack: error.stack
125 | }
126 | };
127 | }
128 | };
129 |
130 | /**
131 | * Tool configuration for getting the content of a Google Apps Script project.
132 | * @type {Object}
133 | */
134 | const apiTool = {
135 | function: executeFunction,
136 | definition: {
137 | type: 'function',
138 | function: {
139 | name: 'script_projects_get_content',
140 | description: 'Get the content of a Google Apps Script project.',
141 | parameters: {
142 | type: 'object',
143 | properties: {
144 | scriptId: {
145 | type: 'string',
146 | description: 'The ID of the script project to retrieve content for.'
147 | },
148 | versionNumber: {
149 | type: 'string',
150 | description: 'The version number of the script project.'
151 | },
152 | fields: {
153 | type: 'string',
154 | description: 'Selector specifying which fields to include in a partial response.'
155 | },
156 | alt: {
157 | type: 'string',
158 | description: 'Data format for response.'
159 | },
160 | key: {
161 | type: 'string',
162 | description: 'API key for the project.'
163 | },
164 | access_token: {
165 | type: 'string',
166 | description: 'OAuth access token.'
167 | },
168 | prettyPrint: {
169 | type: 'string',
170 | description: 'Returns response with indentations and line breaks.'
171 | }
172 | },
173 | required: ['scriptId']
174 | }
175 | }
176 | }
177 | };
178 |
179 | export { apiTool };
```
--------------------------------------------------------------------------------
/test/test-oauth.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Test script to verify OAuth authentication setup
5 | */
6 |
7 | import 'dotenv/config';
8 | import { getOAuthAccessToken, getAuthHeaders } from './lib/oauth-helper.js';
9 |
10 | async function testOAuthAuthentication() {
11 | console.log('🔐 Testing OAuth Authentication for Google Apps Script API...\n');
12 | console.log('🕐 Test started at:', new Date().toISOString());
13 | console.log('📂 Working directory:', process.cwd());
14 | console.log('🔧 Node.js version:', process.version);
15 | console.log('');
16 |
17 | try {
18 | // Test 1: Get access token
19 | console.log('📋 Step 1: Getting OAuth access token...');
20 | console.log('⏳ Attempting to retrieve access token from OAuth helper...');
21 |
22 | const startTime = Date.now();
23 | const accessToken = await getOAuthAccessToken();
24 | const duration = Date.now() - startTime;
25 |
26 | console.log('✅ Successfully obtained access token:', accessToken.substring(0, 20) + '...');
27 | console.log('⏱️ Token retrieval took:', duration + 'ms');
28 | console.log('📏 Full token length:', accessToken.length, 'characters');
29 | console.log('');
30 |
31 | // Test 2: Get auth headers
32 | console.log('📋 Step 2: Creating authorization headers...');
33 | console.log('⏳ Building authorization headers for API requests...');
34 |
35 | const headerStartTime = Date.now();
36 | const headers = await getAuthHeaders();
37 | const headerDuration = Date.now() - headerStartTime;
38 |
39 | console.log('✅ Successfully created auth headers:', JSON.stringify(headers, null, 2));
40 | console.log('⏱️ Header creation took:', headerDuration + 'ms');
41 | console.log('📊 Header keys count:', Object.keys(headers).length);
42 | console.log('');
43 |
44 | // Test 3: Test API call (optional - requires valid script ID)
45 | console.log('📋 Step 3: Testing API connectivity...');
46 | console.log('ℹ️ To test a full API call, you would need a valid script ID.');
47 | console.log('ℹ️ You can test with the script_processes_list tool in your MCP client.\n');
48 |
49 | const totalDuration = Date.now() - startTime;
50 | console.log('🎉 OAuth authentication test completed successfully!');
51 | console.log('✅ Your OAuth setup is working correctly.');
52 | console.log('⏱️ Total test duration:', totalDuration + 'ms');
53 | console.log('🕐 Test completed at:', new Date().toISOString());
54 | console.log('');
55 |
56 | console.log('📝 Next steps:');
57 | console.log('1. Test one of the tools in your MCP client (Claude Desktop, Postman, etc.)');
58 | console.log('2. Use a valid Google Apps Script project ID when calling the tools');
59 | console.log('3. Ensure your OAuth token has the required scopes for the operations you want to perform');
60 |
61 | } catch (error) {
62 | console.error('❌ OAuth authentication failed!');
63 | console.error('🕐 Error occurred at:', new Date().toISOString());
64 | console.error('');
65 |
66 | // Detailed error logging
67 | console.error('📋 Error Details:');
68 | console.error(' 📄 Message:', error.message);
69 | console.error(' 🏷️ Name:', error.name);
70 | console.error(' 📊 Stack trace:');
71 | if (error.stack) {
72 | console.error(error.stack.split('\n').map(line => ' ' + line).join('\n'));
73 | } else {
74 | console.error(' (No stack trace available)');
75 | }
76 | console.error('');
77 |
78 | // Additional error information
79 | if (error.code) {
80 | console.error(' 🔢 Error code:', error.code);
81 | }
82 | if (error.status) {
83 | console.error(' 📊 HTTP status:', error.status);
84 | }
85 | if (error.statusText) {
86 | console.error(' 📝 Status text:', error.statusText);
87 | }
88 | if (error.response) {
89 | console.error(' 📬 Response data:', JSON.stringify(error.response, null, 2));
90 | }
91 | console.error('');
92 |
93 | // Environment check
94 | console.log('🔍 Environment Check:');
95 | console.log(' 📂 Current directory:', process.cwd());
96 | console.log(' 🔧 Node.js version:', process.version);
97 | console.log(' 💾 Platform:', process.platform);
98 | console.log(' 🏗️ Architecture:', process.arch);
99 |
100 | // Check for .env file
101 | try {
102 | const fs = await import('fs');
103 | const envPath = '.env';
104 | const envExists = fs.existsSync(envPath);
105 | console.log(' 📄 .env file exists:', envExists);
106 |
107 | if (envExists) {
108 | const envContent = fs.readFileSync(envPath, 'utf8');
109 | const envLines = envContent.split('\n').filter(line => line.trim() && !line.startsWith('#'));
110 | console.log(' 📋 .env file lines count:', envLines.length);
111 | // Check for required OAuth variables (without showing values)
112 | const requiredVars = ['GOOGLE_APP_SCRIPT_API_CLIENT_ID', 'GOOGLE_APP_SCRIPT_API_CLIENT_SECRET', 'GOOGLE_APP_SCRIPT_API_REFRESH_TOKEN'];
113 | requiredVars.forEach(varName => {
114 | const hasVar = envContent.includes(varName + '=');
115 | console.log(` 🔑 ${varName} present:`, hasVar);
116 | });
117 | }
118 | } catch (fsError) {
119 | console.log(' ⚠️ Could not check .env file:', fsError.message);
120 | }
121 | console.log('');
122 |
123 | console.log('🔧 Troubleshooting steps:');
124 | console.log('1. Check that your .env file contains valid OAuth credentials');
125 | console.log('2. Verify your client ID and client secret are correct');
126 | console.log('3. Ensure your refresh token is valid and not expired');
127 | console.log('4. Follow the OAUTH_SETUP.md guide to obtain new credentials if needed');
128 | console.log('5. Make sure the Google Apps Script API is enabled in your GCP project');
129 | console.log('6. Check your internet connection and firewall settings');
130 | console.log('7. Verify that the oauth-helper.js file exists and is accessible');
131 |
132 | process.exit(1);
133 | }
134 | }
135 |
136 | // Run the test if this script is executed directly
137 | console.log('🔍 Debug: process.argv[1]:', process.argv[1]);
138 | console.log('🔍 Debug: endsWith check:', process.argv[1] && process.argv[1].endsWith('test-oauth.js'));
139 |
140 | if (process.argv[1] && process.argv[1].endsWith('test-oauth.js')) {
141 | console.log('🚀 Starting OAuth test...');
142 | testOAuthAuthentication();
143 | } else {
144 | console.log('❌ Script not executed directly, skipping test');
145 | }
146 |
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-projects-update-content.js:
--------------------------------------------------------------------------------
```javascript
1 | import { getOAuthAccessToken } from '../../../lib/oauth-helper.js';
2 | import { logger } from '../../../lib/logger.js';
3 |
4 | /**
5 | * Function to update the content of a Google Apps Script project.
6 | *
7 | * @param {Object} args - Arguments for the update.
8 | * @param {string} args.scriptId - The ID of the script project to update.
9 | * @param {Array<Object>} args.files - The files to be updated in the script project.
10 | * @returns {Promise<Object>} - The result of the update operation.
11 | */
12 | const executeFunction = async ({ scriptId, files }) => {
13 | const baseUrl = 'https://script.googleapis.com';
14 | const startTime = Date.now();
15 |
16 | try {
17 | logger.info('SCRIPT_UPDATE_CONTENT', 'Starting script content update', {
18 | scriptId,
19 | filesCount: files?.length || 0
20 | });
21 |
22 | // Get OAuth access token
23 | const token = await getOAuthAccessToken();
24 |
25 | // Construct the URL for the request
26 | const url = `${baseUrl}/v1/projects/${scriptId}/content`;
27 |
28 | // Set up headers for the request
29 | const headers = {
30 | 'Authorization': `Bearer ${token}`,
31 | 'Content-Type': 'application/json',
32 | 'Accept': 'application/json'
33 | };
34 |
35 | // Prepare the body of the request
36 | const requestBody = { scriptId, files };
37 | const body = JSON.stringify(requestBody);
38 |
39 | logger.logAPICall('PUT', url, headers, requestBody);
40 |
41 | // Perform the fetch request
42 | const fetchStartTime = Date.now();
43 | const response = await fetch(url, {
44 | method: 'PUT',
45 | headers,
46 | body
47 | });
48 |
49 | const fetchDuration = Date.now() - fetchStartTime;
50 | const responseSize = response.headers.get('content-length') || 'unknown';
51 |
52 | logger.logAPIResponse('PUT', url, response.status, fetchDuration, responseSize);
53 |
54 | // Check if the response was successful
55 | if (!response.ok) {
56 | const errorText = await response.text();
57 | let errorData;
58 |
59 | try {
60 | errorData = JSON.parse(errorText);
61 | } catch (parseError) {
62 | errorData = { message: errorText };
63 | }
64 |
65 | const detailedError = {
66 | status: response.status,
67 | statusText: response.statusText,
68 | url,
69 | errorResponse: errorData,
70 | duration: Date.now() - startTime,
71 | scriptId,
72 | filesCount: files?.length || 0,
73 | timestamp: new Date().toISOString()
74 | };
75 |
76 | logger.error('SCRIPT_UPDATE_CONTENT', 'API request failed', detailedError);
77 |
78 | console.error('❌ API Error Details:', JSON.stringify(detailedError, null, 2));
79 |
80 | throw new Error(`API Error (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
81 | }
82 |
83 | // Parse and return the response data
84 | const data = await response.json();
85 |
86 | logger.info('SCRIPT_UPDATE_CONTENT', 'Successfully updated script content', {
87 | scriptId,
88 | filesCount: files?.length || 0,
89 | duration: Date.now() - startTime
90 | });
91 |
92 | console.log('✅ Successfully updated script content');
93 | return data;
94 | } catch (error) {
95 | const errorDetails = {
96 | message: error.message,
97 | stack: error.stack,
98 | scriptId,
99 | filesCount: files?.length || 0,
100 | duration: Date.now() - startTime,
101 | timestamp: new Date().toISOString(),
102 | errorType: error.name || 'Unknown'
103 | };
104 |
105 | logger.error('SCRIPT_UPDATE_CONTENT', 'Error updating script project content', errorDetails);
106 |
107 | console.error('❌ Error updating script project content:', errorDetails);
108 |
109 | // Return detailed error information for debugging
110 | return {
111 | error: true,
112 | message: error.message,
113 | details: errorDetails,
114 | rawError: {
115 | name: error.name,
116 | stack: error.stack
117 | }
118 | };
119 | }
120 | };
121 |
122 | /**
123 | * Tool configuration for updating Google Apps Script project content.
124 | * @type {Object}
125 | */
126 | const apiTool = {
127 | function: executeFunction,
128 | definition: {
129 | type: 'function',
130 | function: {
131 | name: 'update_script_content',
132 | description: 'Updates the content of a specified Google Apps Script project.',
133 | parameters: {
134 | type: 'object',
135 | properties: {
136 | scriptId: {
137 | type: 'string',
138 | description: 'The ID of the script project to update.'
139 | },
140 | files: {
141 | type: 'array',
142 | items: {
143 | type: 'object',
144 | properties: {
145 | name: {
146 | type: 'string',
147 | description: 'The name of the file.'
148 | },
149 | lastModifyUser: {
150 | type: 'object',
151 | properties: {
152 | photoUrl: { type: 'string' },
153 | domain: { type: 'string' },
154 | name: { type: 'string' },
155 | email: { type: 'string' }
156 | }
157 | },
158 | type: {
159 | type: 'string',
160 | description: 'The type of the file.'
161 | },
162 | updateTime: { type: 'string' },
163 | source: { type: 'string' },
164 | createTime: { type: 'string' },
165 | functionSet: {
166 | type: 'object',
167 | properties: {
168 | values: {
169 | type: 'array',
170 | items: {
171 | type: 'object',
172 | properties: {
173 | parameters: {
174 | type: 'array',
175 | items: {
176 | type: 'object',
177 | properties: {
178 | value: { type: 'string' }
179 | }
180 | }
181 | },
182 | name: { type: 'string' }
183 | }
184 | }
185 | }
186 | }
187 | }
188 | }
189 | },
190 | description: 'The files to be updated in the script project.'
191 | }
192 | },
193 | required: ['scriptId', 'files']
194 | }
195 | }
196 | }
197 | };
198 |
199 | export { apiTool };
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-projects-versions-list.js:
--------------------------------------------------------------------------------
```javascript
1 | import { getOAuthAccessToken } from '../../../lib/oauth-helper.js';
2 | import { logger } from '../../../lib/logger.js';
3 |
4 | /**
5 | * Function to list the versions of a Google Apps Script project.
6 | *
7 | * @param {Object} args - Arguments for the request.
8 | * @param {string} args.scriptId - The ID of the script project.
9 | * @param {number} [args.pageSize=100] - The number of versions to return per page.
10 | * @param {string} [args.pageToken] - The token for the next page of results.
11 | * @param {string} [args.fields] - Selector specifying which fields to include in a partial response.
12 | * @param {string} [args.alt='json'] - Data format for response.
13 | * @param {string} [args.key] - API key for the request.
14 | * @param {string} [args.access_token] - OAuth access token.
15 | * @param {string} [args.oauth_token] - OAuth 2.0 token for the current user.
16 | * @param {boolean} [args.prettyPrint=true] - Returns response with indentations and line breaks.
17 | * @returns {Promise<Object>} - The result of the request containing the versions of the script project.
18 | */
19 | const executeFunction = async ({ scriptId, pageSize = 100, pageToken, fields, key, access_token, oauth_token, prettyPrint = true }) => {
20 | const baseUrl = 'https://script.googleapis.com';
21 | const startTime = Date.now();
22 |
23 | try {
24 | logger.info('SCRIPT_VERSIONS_LIST', 'Starting script versions list request', { scriptId, pageSize, pageToken });
25 |
26 | // Get OAuth access token
27 | const token = await getOAuthAccessToken();
28 |
29 | // Construct the URL with query parameters
30 | const url = new URL(`${baseUrl}/v1/projects/${scriptId}/versions`);
31 | url.searchParams.append('pageSize', pageSize.toString());
32 | if (pageToken) url.searchParams.append('pageToken', pageToken);
33 | if (fields) url.searchParams.append('fields', fields);
34 | url.searchParams.append('alt', 'json');
35 | if (key) url.searchParams.append('key', key);
36 | if (prettyPrint) url.searchParams.append('prettyPrint', prettyPrint.toString());
37 |
38 | logger.debug('SCRIPT_VERSIONS_LIST', 'Constructed API URL', {
39 | url: url.toString(),
40 | pathSegments: url.pathname.split('/'),
41 | queryParams: Object.fromEntries(url.searchParams)
42 | });
43 |
44 | // Set up headers for the request
45 | const headers = {
46 | 'Accept': 'application/json',
47 | 'Authorization': `Bearer ${token}`
48 | };
49 |
50 | logger.logAPICall('GET', url.toString(), headers);
51 |
52 | // Perform the fetch request
53 | const fetchStartTime = Date.now();
54 | const response = await fetch(url.toString(), {
55 | method: 'GET',
56 | headers
57 | });
58 |
59 | const fetchDuration = Date.now() - fetchStartTime;
60 | const responseSize = response.headers.get('content-length') || 'unknown';
61 |
62 | logger.logAPIResponse('GET', url.toString(), response.status, fetchDuration, responseSize);
63 |
64 | // Check if the response was successful
65 | if (!response.ok) {
66 | const errorText = await response.text();
67 | let errorData;
68 |
69 | try {
70 | errorData = JSON.parse(errorText);
71 | } catch (parseError) {
72 | errorData = { message: errorText };
73 | }
74 |
75 | const detailedError = {
76 | status: response.status,
77 | statusText: response.statusText,
78 | url: url.toString(),
79 | errorResponse: errorData,
80 | duration: Date.now() - startTime,
81 | scriptId,
82 | timestamp: new Date().toISOString()
83 | };
84 |
85 | logger.error('SCRIPT_VERSIONS_LIST', 'API request failed', detailedError);
86 |
87 | console.error('❌ API Error Details:', JSON.stringify(detailedError, null, 2));
88 |
89 | throw new Error(`API Error (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
90 | }
91 |
92 | // Parse and return the response data
93 | const data = await response.json();
94 |
95 | logger.info('SCRIPT_VERSIONS_LIST', 'Successfully retrieved script versions', {
96 | scriptId,
97 | versionsCount: data.versions?.length || 0,
98 | duration: Date.now() - startTime
99 | });
100 |
101 | console.log('✅ Successfully retrieved script versions');
102 | return data;
103 | } catch (error) {
104 | const errorDetails = {
105 | message: error.message,
106 | stack: error.stack,
107 | scriptId,
108 | duration: Date.now() - startTime,
109 | timestamp: new Date().toISOString(),
110 | errorType: error.name || 'Unknown'
111 | };
112 |
113 | logger.error('SCRIPT_VERSIONS_LIST', 'Error listing script versions', errorDetails);
114 |
115 | console.error('❌ Error listing script versions:', errorDetails);
116 |
117 | // Return detailed error information for debugging
118 | return {
119 | error: true,
120 | message: error.message,
121 | details: errorDetails,
122 | rawError: {
123 | name: error.name,
124 | stack: error.stack
125 | }
126 | };
127 | }
128 | };
129 |
130 | /**
131 | * Tool configuration for listing versions of a Google Apps Script project.
132 | * @type {Object}
133 | */
134 | const apiTool = {
135 | function: executeFunction,
136 | definition: {
137 | type: 'function',
138 | function: {
139 | name: 'script_projects_versions_list',
140 | description: 'List the versions of a Google Apps Script project.',
141 | parameters: {
142 | type: 'object',
143 | properties: {
144 | scriptId: {
145 | type: 'string',
146 | description: 'The ID of the script project.'
147 | },
148 | pageSize: {
149 | type: 'integer',
150 | description: 'The number of versions to return per page.'
151 | },
152 | pageToken: {
153 | type: 'string',
154 | description: 'The token for the next page of results.'
155 | },
156 | fields: {
157 | type: 'string',
158 | description: 'Selector specifying which fields to include in a partial response.'
159 | },
160 | alt: {
161 | type: 'string',
162 | enum: ['json'],
163 | description: 'Data format for response.'
164 | },
165 | key: {
166 | type: 'string',
167 | description: 'API key for the request.'
168 | },
169 | access_token: {
170 | type: 'string',
171 | description: 'OAuth access token.'
172 | },
173 | oauth_token: {
174 | type: 'string',
175 | description: 'OAuth 2.0 token for the current user.'
176 | },
177 | prettyPrint: {
178 | type: 'boolean',
179 | description: 'Returns response with indentations and line breaks.'
180 | }
181 | },
182 | required: ['scriptId']
183 | }
184 | }
185 | }
186 | };
187 |
188 | export { apiTool };
```
--------------------------------------------------------------------------------
/test/test-mcp-errors.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Comprehensive MCP Server Error Testing Script
5 | * Tests the script processes list tool with real script ID to identify specific errors
6 | */
7 |
8 | import { discoverTools } from './lib/tools.js';
9 | import { logger } from './lib/logger.js';
10 |
11 | // Real script ID used throughout the codebase
12 | const REAL_SCRIPT_ID = '1fSY7y3Rh84FsgJmrFIMm4AUOV3mPgelLRvZ4Dahrv68zyDzX-cGbeYjn';
13 |
14 | async function testMCPErrors() {
15 | console.log('🚨 MCP Server Error Analysis - Script Processes List\n');
16 |
17 | try {
18 | // Set high verbosity for maximum details
19 | process.env.LOG_LEVEL = 'trace';
20 |
21 | // Discover tools
22 | logger.info('TEST', 'Starting comprehensive error analysis');
23 | const tools = await discoverTools();
24 |
25 | // Find the script processes list tool
26 | const processListTool = tools.find(tool =>
27 | tool.definition?.function?.name === 'script_processes_list'
28 | );
29 |
30 | if (!processListTool) {
31 | console.error('❌ script_processes_list tool not found');
32 | return;
33 | }
34 |
35 | console.log('🔍 Found script_processes_list tool');
36 | console.log(`📝 Description: ${processListTool.definition.function.description}`);
37 | console.log(`🔧 Required: ${processListTool.definition.function.parameters.required.join(', ')}`);
38 | console.log(`📋 Properties: ${Object.keys(processListTool.definition.function.parameters.properties).join(', ')}\n`);
39 |
40 | // TEST 1: Basic call with real script ID
41 | console.log('🧪 TEST 1: Basic call with real script ID');
42 | console.log(`🎯 Using script ID: ${REAL_SCRIPT_ID}`);
43 | try {
44 | const result1 = await processListTool.function({
45 | scriptId: REAL_SCRIPT_ID
46 | });
47 | console.log('✅ Success - Basic call worked');
48 | console.log(`📊 Result type: ${typeof result1}`);
49 | console.log(`📊 Result content: ${JSON.stringify(result1, null, 2)}`);
50 | } catch (error) {
51 | console.log('❌ Error in basic call:');
52 | console.log(` Message: ${error.message}`);
53 | console.log(` Type: ${error.constructor.name}`);
54 | }
55 |
56 | // TEST 2: Call with minimal valid fields parameter
57 | console.log('\n🧪 TEST 2: Call with valid fields parameter');
58 | try {
59 | const result2 = await processListTool.function({
60 | scriptId: REAL_SCRIPT_ID,
61 | fields: 'processes'
62 | });
63 | console.log('✅ Success - With fields parameter');
64 | console.log(`📊 Result: ${JSON.stringify(result2, null, 2)}`);
65 | } catch (error) {
66 | console.log('❌ Error with fields parameter:');
67 | console.log(` Message: ${error.message}`);
68 | console.log(` Type: ${error.constructor.name}`);
69 | }
70 |
71 | // TEST 3: Call with specific valid field selections
72 | console.log('\n🧪 TEST 3: Call with specific field selections');
73 | const validFields = [
74 | 'processes(processType,functionName,startTime,duration)',
75 | 'processes(processType,functionName)',
76 | 'processes(startTime)',
77 | 'processes.processType,processes.functionName'
78 | ];
79 |
80 | for (const field of validFields) {
81 | console.log(`\n 📝 Testing field: ${field}`);
82 | try {
83 | const result = await processListTool.function({
84 | scriptId: REAL_SCRIPT_ID,
85 | fields: field,
86 | pageSize: 3
87 | });
88 | console.log(` ✅ Success with field: ${field}`);
89 | console.log(` 📊 Result: ${JSON.stringify(result, null, 2)}`);
90 | } catch (error) {
91 | console.log(` ❌ Error with field '${field}': ${error.message}`);
92 | }
93 | }
94 |
95 | // TEST 4: Test different parameter combinations
96 | console.log('\n🧪 TEST 4: Testing different parameter combinations');
97 |
98 | const testCases = [
99 | { name: 'With pageSize only', params: { scriptId: REAL_SCRIPT_ID, pageSize: 5 } },
100 | { name: 'With pageToken', params: { scriptId: REAL_SCRIPT_ID, pageToken: 'test_token' } },
101 | { name: 'With deploymentId filter', params: { scriptId: REAL_SCRIPT_ID, deploymentId: 'test_deployment' } },
102 | { name: 'With functionName filter', params: { scriptId: REAL_SCRIPT_ID, functionName: 'myFunction' } },
103 | { name: 'With time filters', params: {
104 | scriptId: REAL_SCRIPT_ID,
105 | startTime: '2024-01-01T00:00:00Z',
106 | endTime: '2024-12-31T23:59:59Z'
107 | }},
108 | ];
109 |
110 | for (const testCase of testCases) {
111 | console.log(`\n 📝 Testing: ${testCase.name}`);
112 | try {
113 | const result = await processListTool.function(testCase.params);
114 | console.log(` ✅ Success: ${testCase.name}`);
115 | console.log(` 📊 Result: ${JSON.stringify(result, null, 2)}`);
116 | } catch (error) {
117 | console.log(` ❌ Error in ${testCase.name}: ${error.message}`);
118 | }
119 | }
120 |
121 | // TEST 5: Check if the script actually exists by trying to get its metadata
122 | console.log('\n🧪 TEST 5: Verify script exists by getting metadata');
123 | const scriptGetTool = tools.find(tool =>
124 | tool.definition?.function?.name === 'script_projects_get'
125 | );
126 |
127 | if (scriptGetTool) {
128 | try {
129 | const metadata = await scriptGetTool.function({
130 | scriptId: REAL_SCRIPT_ID
131 | });
132 | console.log('✅ Script metadata retrieved successfully');
133 | console.log(`📋 Script title: ${metadata.title || 'No title'}`);
134 | console.log(`📋 Script ID: ${metadata.scriptId || 'No ID'}`);
135 | console.log(`📋 Create time: ${metadata.createTime || 'No create time'}`);
136 | } catch (error) {
137 | console.log('❌ Error getting script metadata:');
138 | console.log(` Message: ${error.message}`);
139 | console.log(' This might indicate the script doesn\'t exist or access issues');
140 | }
141 | }
142 |
143 | } catch (error) {
144 | logger.error('TEST', 'Test failed with unexpected error', {
145 | error: {
146 | message: error.message,
147 | stack: error.stack
148 | }
149 | });
150 | console.error('❌ Test failed:', error.message);
151 | }
152 |
153 | console.log('\n🏁 Error Analysis Complete');
154 | console.log('\n📋 Summary of Findings:');
155 | console.log(' • Check the detailed logs above for specific API errors');
156 | console.log(' • Look for authentication issues, invalid parameters, or API limitations');
157 | console.log(' • Note which parameter combinations work vs. which fail');
158 | console.log(' • Verify if the script ID is accessible with current OAuth scopes');
159 | }
160 |
161 | // Run the error analysis
162 | testMCPErrors().catch(console.error);
163 |
```
--------------------------------------------------------------------------------
/test/oauth-setup-fixed.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * OAuth Setup Script for Google Apps Script API
5 | * This script helps you obtain and securely store OAuth tokens
6 | */
7 |
8 | import 'dotenv/config';
9 | import { manualOAuthFlow } from '../lib/oauth-helper.js';
10 | import { TokenManager } from '../lib/tokenManager.js';
11 | import { readFileSync } from 'fs';
12 |
13 | console.log('🔐 Google Apps Script API OAuth Setup');
14 | console.log('=====================================\n');
15 |
16 | async function setupOAuth() {
17 | console.log('📋 This script will help you set up OAuth authentication for Google Apps Script API.');
18 | console.log('📝 You need to have your CLIENT_ID and CLIENT_SECRET configured in .env file.\n');
19 |
20 | const tokenManager = new TokenManager();
21 |
22 | // Handle info command
23 | if (process.argv.includes('--info')) {
24 | const tokenInfo = tokenManager.getTokenInfo();
25 |
26 | console.log('🔍 Token Information:');
27 | console.log('=====================\n');
28 |
29 | if (tokenInfo.hasTokens) {
30 | console.log('✅ Tokens found');
31 | console.log(`📁 Location: ${tokenInfo.location}`);
32 | console.log(`💾 Saved at: ${tokenInfo.savedAt}`);
33 | console.log(`⏰ Expires at: ${tokenInfo.expiresAt}`);
34 | console.log(`📊 Status: ${tokenInfo.status}`);
35 | console.log(`🔐 Scope: ${tokenInfo.scope || 'Not specified'}`);
36 | } else {
37 | console.log('❌ No tokens found');
38 | console.log(`📁 Expected location: ${tokenInfo.location}`);
39 | console.log('\n💡 Run "node oauth-setup.js" to set up OAuth tokens');
40 | }
41 |
42 | process.exit(0);
43 | }
44 |
45 | // Handle clear command
46 | if (process.argv.includes('--clear')) {
47 | tokenManager.clearTokens();
48 | console.log('✅ Tokens cleared successfully.');
49 | process.exit(0);
50 | }
51 |
52 | // Check if tokens already exist
53 | const tokenInfo = tokenManager.getTokenInfo();
54 | if (tokenInfo.hasTokens) {
55 | console.log('🔍 Found existing tokens:');
56 | console.log(` 📁 Location: ${tokenInfo.location}`);
57 | console.log(` 💾 Saved at: ${tokenInfo.savedAt}`);
58 | console.log(` ⏰ Expires at: ${tokenInfo.expiresAt}`);
59 | console.log(` 📊 Status: ${tokenInfo.status}`);
60 | console.log(` 🔐 Scope: ${tokenInfo.scope || 'Not specified'}\n`);
61 |
62 | if (!tokenInfo.isExpired) {
63 | console.log('✅ You already have valid tokens stored.');
64 | console.log('💡 To get new tokens, run: node oauth-setup.js --force');
65 | console.log('🗑️ To clear existing tokens, run: node oauth-setup.js --clear\n');
66 |
67 | if (!process.argv.includes('--force')) {
68 | process.exit(0);
69 | }
70 | }
71 | }
72 |
73 | try {
74 | // Check if .env file exists and has required credentials
75 | const envPath = '.env';
76 | let envContent = '';
77 |
78 | try {
79 | envContent = readFileSync(envPath, 'utf8');
80 | console.log('✅ Found .env file');
81 | } catch (error) {
82 | console.error('❌ No .env file found. Please create one first with your CLIENT_ID and CLIENT_SECRET.');
83 | console.log('\n📝 Example .env file content:');
84 | console.log('GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_client_id_here');
85 | console.log('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_client_secret_here');
86 | console.log('\n📖 Note: Refresh token is now stored securely and not needed in .env file');
87 | process.exit(1);
88 | }
89 |
90 | // Check for required credentials
91 | const hasClientId = envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_ID=') &&
92 | !envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_client_id_here');
93 | const hasClientSecret = envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=') &&
94 | !envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_client_secret_here');
95 |
96 | if (!hasClientId || !hasClientSecret) {
97 | console.error('❌ Missing CLIENT_ID or CLIENT_SECRET in .env file.');
98 | console.log('\n🔧 Please update your .env file with valid credentials:');
99 | console.log(' - GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_actual_client_id');
100 | console.log(' - GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_actual_client_secret');
101 | console.log('\n📖 See OAUTH_SETUP.md for instructions on obtaining these credentials.');
102 | process.exit(1);
103 | }
104 |
105 | console.log('✅ Found required credentials in .env file');
106 | console.log('\n🚀 Starting OAuth flow...');
107 | console.log('📱 Your browser will open automatically');
108 | console.log('🔐 Please authorize the application when prompted');
109 | console.log('⏳ Waiting for authorization...\n');
110 |
111 | // Start OAuth flow
112 | const tokens = await manualOAuthFlow();
113 |
114 | if (tokens.refresh_token) {
115 | console.log('\n🎉 OAuth setup successful!');
116 | console.log('🔑 Access token obtained:', tokens.access_token ? '✅' : '❌');
117 | console.log('🔄 Refresh token obtained:', tokens.refresh_token ? '✅' : '❌');
118 |
119 | // Save tokens securely using TokenManager
120 | try {
121 | tokenManager.saveTokens(tokens);
122 | console.log('💾 Tokens saved securely');
123 |
124 | const tokenInfo = tokenManager.getTokenInfo();
125 | console.log(`📁 Token location: ${tokenInfo.location}`);
126 | console.log(`🔒 File permissions: Owner read/write only`);
127 |
128 | console.log('\n✅ Setup complete! Your OAuth tokens are now stored securely.');
129 | console.log('🔐 Refresh tokens are stored in a secure OS-specific location');
130 | console.log('🚀 You can now use the MCP server and API tools');
131 |
132 | console.log('\n🧪 Test your setup with:');
133 | console.log(' node test-token-management.js');
134 |
135 | } catch (saveError) {
136 | console.error('\n❌ Failed to save tokens:', saveError.message);
137 | console.log('🔧 Please check file permissions and try again');
138 | process.exit(1);
139 | }
140 |
141 | } else {
142 | console.log('\n⚠️ OAuth completed but no refresh token received.');
143 | console.log('🔄 You may need to revoke and re-authorize the application.');
144 | console.log('📖 Check the Google Cloud Console for your OAuth settings.');
145 | }
146 |
147 | } catch (error) {
148 | console.error('\n❌ OAuth setup failed:', error.message);
149 | console.log('\n🔧 Troubleshooting:');
150 | console.log(' 1. Check your internet connection');
151 | console.log(' 2. Verify your CLIENT_ID and CLIENT_SECRET are correct');
152 | console.log(' 3. Ensure the redirect URI is registered in Google Cloud Console');
153 | console.log(' 4. Make sure Google Apps Script API is enabled');
154 | console.log(' 5. Try revoking and re-creating your OAuth credentials');
155 | console.log('\n📖 For detailed setup instructions, see OAUTH_SETUP.md');
156 | process.exit(1);
157 | }
158 | }
159 |
160 | // Run setup
161 | setupOAuth().catch((error) => {
162 | console.error('💥 Unexpected error:', error);
163 | process.exit(1);
164 | });
165 |
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-processes-list-script-processes.js:
--------------------------------------------------------------------------------
```javascript
1 | import { logger } from '../../../lib/logger.js';
2 |
3 | /**
4 | * Function to list script processes for a given script ID.
5 | *
6 | * @param {Object} args - Arguments for listing script processes.
7 | * @param {string} args.scriptId - The ID of the script to list processes for.
8 | * @param {number} [args.pageSize=100] - The number of processes to return per page.
9 | * @param {string} [args.functionName] - Filter by function name.
10 | * @param {string} [args.pageToken] - Token for pagination.
11 | * @param {string} [args.startTime] - Filter by start time.
12 | * @param {string} [args.endTime] - Filter by end time.
13 | * @param {string} [args.deploymentId] - Filter by deployment ID.
14 | * @param {string} [args.types] - Filter by process types.
15 | * @param {string} [args.statuses] - Filter by process statuses.
16 | * @param {string} [args.userAccessLevels] - Filter by user access levels.
17 | * @returns {Promise<Object>} - The result of the script processes listing.
18 | */
19 | const executeFunction = async ({ scriptId, pageSize = 100, functionName, pageToken, startTime, endTime, deploymentId, types, statuses, userAccessLevels }) => {
20 | const baseUrl = 'https://script.googleapis.com';
21 | const accessToken = ''; // will be provided by the user
22 | const startTimeMs = Date.now();
23 |
24 | try {
25 | logger.info('SCRIPT_PROCESSES_LIST', 'Starting script processes list request', { scriptId, pageSize, functionName });
26 |
27 | // Construct the URL with query parameters
28 | const url = new URL(`${baseUrl}/v1/processes:listScriptProcesses`);
29 | url.searchParams.append('scriptId', scriptId);
30 | url.searchParams.append('pageSize', pageSize.toString());
31 | if (functionName) url.searchParams.append('scriptProcessFilter.functionName', functionName);
32 | if (pageToken) url.searchParams.append('pageToken', pageToken);
33 | if (startTime) url.searchParams.append('scriptProcessFilter.startTime', startTime);
34 | if (endTime) url.searchParams.append('scriptProcessFilter.endTime', endTime);
35 | if (deploymentId) url.searchParams.append('scriptProcessFilter.deploymentId', deploymentId);
36 | if (types) url.searchParams.append('scriptProcessFilter.types', types);
37 | if (statuses) url.searchParams.append('scriptProcessFilter.statuses', statuses);
38 | if (userAccessLevels) url.searchParams.append('scriptProcessFilter.userAccessLevels', userAccessLevels);
39 | url.searchParams.append('alt', 'json');
40 | url.searchParams.append('prettyPrint', 'true');
41 |
42 | logger.debug('SCRIPT_PROCESSES_LIST', 'Constructed API URL', {
43 | url: url.toString(),
44 | pathSegments: url.pathname.split('/'),
45 | queryParams: Object.fromEntries(url.searchParams)
46 | });
47 |
48 | // Set up headers for the request
49 | const headers = {
50 | 'Authorization': `Bearer ${accessToken}`,
51 | 'Accept': 'application/json'
52 | };
53 |
54 | logger.logAPICall('GET', url.toString(), headers);
55 |
56 | // Perform the fetch request
57 | const fetchStartTime = Date.now();
58 | const response = await fetch(url.toString(), {
59 | method: 'GET',
60 | headers
61 | });
62 |
63 | const fetchDuration = Date.now() - fetchStartTime;
64 | const responseSize = response.headers.get('content-length') || 'unknown';
65 |
66 | logger.logAPIResponse('GET', url.toString(), response.status, fetchDuration, responseSize);
67 |
68 | // Check if the response was successful
69 | if (!response.ok) {
70 | const errorText = await response.text();
71 | let errorData;
72 |
73 | try {
74 | errorData = JSON.parse(errorText);
75 | } catch (parseError) {
76 | errorData = { message: errorText };
77 | }
78 |
79 | const detailedError = {
80 | status: response.status,
81 | statusText: response.statusText,
82 | url: url.toString(),
83 | errorResponse: errorData,
84 | duration: Date.now() - startTimeMs,
85 | scriptId,
86 | timestamp: new Date().toISOString()
87 | };
88 |
89 | logger.error('SCRIPT_PROCESSES_LIST', 'API request failed', detailedError);
90 |
91 | console.error('❌ API Error Details:', JSON.stringify(detailedError, null, 2));
92 |
93 | throw new Error(`API Error (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
94 | }
95 |
96 | // Parse and return the response data
97 | const data = await response.json();
98 |
99 | logger.info('SCRIPT_PROCESSES_LIST', 'Successfully retrieved script processes', {
100 | scriptId,
101 | processesCount: data.processes?.length || 0,
102 | duration: Date.now() - startTimeMs
103 | });
104 |
105 | console.log('✅ Successfully retrieved script processes');
106 | return data;
107 | } catch (error) {
108 | const errorDetails = {
109 | message: error.message,
110 | stack: error.stack,
111 | scriptId,
112 | duration: Date.now() - startTimeMs,
113 | timestamp: new Date().toISOString(),
114 | errorType: error.name || 'Unknown'
115 | };
116 |
117 | logger.error('SCRIPT_PROCESSES_LIST', 'Error listing script processes', errorDetails);
118 |
119 | console.error('❌ Error listing script processes:', errorDetails);
120 |
121 | // Return detailed error information for debugging
122 | return {
123 | error: true,
124 | message: error.message,
125 | details: errorDetails,
126 | rawError: {
127 | name: error.name,
128 | stack: error.stack
129 | }
130 | };
131 | }
132 | };
133 |
134 | /**
135 | * Tool configuration for listing script processes on Google Apps Script.
136 | * @type {Object}
137 | */
138 | const apiTool = {
139 | function: executeFunction,
140 | definition: {
141 | type: 'function',
142 | function: {
143 | name: 'list_script_processes',
144 | description: 'List information about a script\'s executed processes.',
145 | parameters: {
146 | type: 'object',
147 | properties: {
148 | scriptId: {
149 | type: 'string',
150 | description: 'The ID of the script to list processes for.'
151 | },
152 | pageSize: {
153 | type: 'integer',
154 | description: 'The number of processes to return per page.'
155 | },
156 | functionName: {
157 | type: 'string',
158 | description: 'Filter by function name.'
159 | },
160 | pageToken: {
161 | type: 'string',
162 | description: 'Token for pagination.'
163 | },
164 | startTime: {
165 | type: 'string',
166 | description: 'Filter by start time.'
167 | },
168 | endTime: {
169 | type: 'string',
170 | description: 'Filter by end time.'
171 | },
172 | deploymentId: {
173 | type: 'string',
174 | description: 'Filter by deployment ID.'
175 | },
176 | types: {
177 | type: 'string',
178 | description: 'Filter by process types.'
179 | },
180 | statuses: {
181 | type: 'string',
182 | description: 'Filter by process statuses.'
183 | },
184 | userAccessLevels: {
185 | type: 'string',
186 | description: 'Filter by user access levels.'
187 | }
188 | },
189 | required: ['scriptId']
190 | }
191 | }
192 | }
193 | };
194 |
195 | export { apiTool };
```
--------------------------------------------------------------------------------
/lib/tokenManager.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * OAuth Token Manager for secure token storage and management
3 | * Handles access token refresh and secure storage of refresh tokens
4 | */
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import os from 'os';
9 |
10 | export class TokenManager {
11 | constructor() {
12 | this.tokenDir = this.getTokenDirectory();
13 | this.tokenFile = path.join(this.tokenDir, 'tokens.json');
14 | this.ensureTokenDirectory();
15 | }
16 |
17 | /**
18 | * Get platform-specific directory for token storage
19 | * @returns {string} Token directory path
20 | */
21 | getTokenDirectory() {
22 | const platform = os.platform();
23 | const homedir = os.homedir();
24 |
25 | switch (platform) {
26 | case 'win32':
27 | return path.join(homedir, 'AppData', 'Roaming', 'google-apps-script-mcp');
28 | case 'darwin':
29 | return path.join(homedir, 'Library', 'Application Support', 'google-apps-script-mcp');
30 | default:
31 | return path.join(homedir, '.config', 'google-apps-script-mcp');
32 | }
33 | }
34 |
35 | /**
36 | * Ensure token directory exists
37 | */
38 | ensureTokenDirectory() {
39 | if (!fs.existsSync(this.tokenDir)) {
40 | fs.mkdirSync(this.tokenDir, { recursive: true });
41 | console.log(`📁 Created token directory: ${this.tokenDir}`);
42 | }
43 | }
44 |
45 | /**
46 | * Save OAuth tokens securely
47 | * @param {Object} tokens - OAuth tokens object
48 | */
49 | saveTokens(tokens) {
50 | const tokenData = {
51 | access_token: tokens.access_token,
52 | refresh_token: tokens.refresh_token,
53 | expires_at: Date.now() + (tokens.expires_in * 1000),
54 | token_type: tokens.token_type || 'Bearer',
55 | scope: tokens.scope,
56 | saved_at: new Date().toISOString()
57 | };
58 |
59 | try {
60 | fs.writeFileSync(this.tokenFile, JSON.stringify(tokenData, null, 2), { mode: 0o600 });
61 | console.log(`💾 Tokens saved securely to: ${this.tokenFile}`);
62 | console.log(`🔒 File permissions: 600 (owner read/write only)`);
63 | } catch (error) {
64 | console.error('❌ Failed to save tokens:', error.message);
65 | throw new Error(`Failed to save tokens: ${error.message}`);
66 | }
67 | }
68 |
69 | /**
70 | * Load stored OAuth tokens
71 | * @returns {Object|null} Stored tokens or null if not found
72 | */
73 | loadTokens() {
74 | if (!fs.existsSync(this.tokenFile)) {
75 | console.log('📝 No stored tokens found');
76 | return null;
77 | }
78 |
79 | try {
80 | const tokenData = JSON.parse(fs.readFileSync(this.tokenFile, 'utf8'));
81 | console.log(`📖 Loaded tokens from: ${this.tokenFile}`);
82 | console.log(`💾 Tokens saved at: ${tokenData.saved_at || 'Unknown'}`);
83 | return tokenData;
84 | } catch (error) {
85 | console.error('❌ Failed to load tokens:', error.message);
86 | return null;
87 | }
88 | }
89 |
90 | /**
91 | * Check if current access token is expired or will expire soon
92 | * @param {Object} tokens - Token object
93 | * @returns {boolean} True if token is expired or will expire within 1 minute
94 | */
95 | isTokenExpired(tokens) {
96 | if (!tokens || !tokens.expires_at) {
97 | return true;
98 | }
99 |
100 | // Consider token expired if it expires within the next minute
101 | const bufferTime = 60000; // 1 minute buffer
102 | const isExpired = Date.now() >= (tokens.expires_at - bufferTime);
103 |
104 | if (isExpired) {
105 | console.log('⏰ Access token is expired or will expire soon');
106 | } else {
107 | const expiresIn = Math.round((tokens.expires_at - Date.now()) / 1000 / 60);
108 | console.log(`⏰ Access token expires in ${expiresIn} minutes`);
109 | }
110 |
111 | return isExpired;
112 | }
113 |
114 | /**
115 | * Refresh access token using stored refresh token
116 | * @param {string} clientId - OAuth client ID
117 | * @param {string} clientSecret - OAuth client secret
118 | * @returns {Promise<Object>} New tokens
119 | */
120 | async refreshAccessToken(clientId, clientSecret) {
121 | const tokens = this.loadTokens();
122 | if (!tokens || !tokens.refresh_token) {
123 | throw new Error('No refresh token available. Please run OAuth setup again: node oauth-setup.js');
124 | }
125 |
126 | console.log('🔄 Refreshing access token...');
127 |
128 | try {
129 | const response = await fetch('https://oauth2.googleapis.com/token', {
130 | method: 'POST',
131 | headers: {
132 | 'Content-Type': 'application/x-www-form-urlencoded',
133 | },
134 | body: new URLSearchParams({
135 | client_id: clientId,
136 | client_secret: clientSecret,
137 | refresh_token: tokens.refresh_token,
138 | grant_type: 'refresh_token'
139 | })
140 | });
141 |
142 | if (!response.ok) {
143 | const errorText = await response.text();
144 | console.error('❌ Token refresh failed:', response.status, errorText);
145 | throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
146 | }
147 |
148 | const newTokens = await response.json();
149 |
150 | // Keep the existing refresh token if not provided in response
151 | if (!newTokens.refresh_token) {
152 | newTokens.refresh_token = tokens.refresh_token;
153 | }
154 |
155 | // Save the refreshed tokens
156 | this.saveTokens(newTokens);
157 | console.log('✅ Access token refreshed successfully');
158 |
159 | return newTokens;
160 | } catch (error) {
161 | console.error('❌ Failed to refresh access token:', error.message);
162 | throw error;
163 | }
164 | }
165 |
166 | /**
167 | * Get a valid access token, refreshing if necessary
168 | * @param {string} clientId - OAuth client ID
169 | * @param {string} clientSecret - OAuth client secret
170 | * @returns {Promise<string>} Valid access token
171 | */
172 | async getValidAccessToken(clientId, clientSecret) {
173 | let tokens = this.loadTokens();
174 |
175 | if (!tokens) {
176 | throw new Error('No tokens found. Please run OAuth setup first: node oauth-setup.js');
177 | }
178 |
179 | if (this.isTokenExpired(tokens)) {
180 | console.log('🔄 Token expired, refreshing...');
181 | tokens = await this.refreshAccessToken(clientId, clientSecret);
182 | } else {
183 | console.log('✅ Using existing valid access token');
184 | }
185 |
186 | return tokens.access_token;
187 | }
188 |
189 | /**
190 | * Check if tokens are stored and available
191 | * @returns {boolean} True if refresh token is available
192 | */
193 | hasStoredTokens() {
194 | const tokens = this.loadTokens();
195 | return tokens && tokens.refresh_token;
196 | }
197 |
198 | /**
199 | * Clear stored tokens (for logout/reset)
200 | */
201 | clearTokens() {
202 | if (fs.existsSync(this.tokenFile)) {
203 | fs.unlinkSync(this.tokenFile);
204 | console.log('🗑️ Stored tokens cleared');
205 | }
206 | }
207 |
208 | /**
209 | * Get token storage information
210 | * @returns {Object} Token storage info
211 | */
212 | getTokenInfo() {
213 | const tokens = this.loadTokens();
214 | if (!tokens) {
215 | return {
216 | hasTokens: false,
217 | location: this.tokenFile,
218 | status: 'No tokens stored'
219 | };
220 | }
221 |
222 | const isExpired = this.isTokenExpired(tokens);
223 | const expiresAt = new Date(tokens.expires_at);
224 |
225 | return {
226 | hasTokens: true,
227 | location: this.tokenFile,
228 | savedAt: tokens.saved_at,
229 | expiresAt: expiresAt.toISOString(),
230 | isExpired,
231 | scope: tokens.scope,
232 | status: isExpired ? 'Token expired' : 'Token valid'
233 | };
234 | }
235 | }
236 |
237 | // Export using named export (already done at class declaration)
238 |
```
--------------------------------------------------------------------------------
/tools/google-app-script-api/apps-script-api/script-processes-list.js:
--------------------------------------------------------------------------------
```javascript
1 | import { getAuthHeaders } from '../../../lib/oauth-helper.js';
2 | import { logger } from '../../../lib/logger.js';
3 |
4 | /**
5 | * Function to list processes for a Google Apps Script project.
6 | *
7 | * @param {Object} args - Arguments for the process listing.
8 | * @param {string} args.scriptId - The ID of the script to filter processes.
9 | * @param {string} [args.startTime] - The start time for filtering processes.
10 | * @param {string} [args.functionName] - The name of the function to filter processes.
11 | * @param {string} [args.deploymentId] - The deployment ID to filter processes.
12 | * @param {string} [args.projectName] - The project name to filter processes.
13 | * @param {Array<string>} [args.statuses] - The statuses to filter processes.
14 | * @param {string} [args.pageToken] - Token for pagination.
15 | * @param {Array<string>} [args.types] - The types of processes to filter.
16 | * @param {Array<string>} [args.userAccessLevels] - User access levels to filter.
17 | * @param {number} [args.pageSize=100] - The number of processes to return per page.
18 | * @param {string} [args.endTime] - The end time for filtering processes.
19 | * @param {string} [args.fields] - Selector specifying which fields to include in a partial response.
20 | * @param {boolean} [args.prettyPrint=true] - Returns response with indentations and line breaks.
21 | * @returns {Promise<Object>} - The result of the process listing.
22 | */
23 | const executeFunction = async ({
24 | scriptId,
25 | startTime,
26 | functionName,
27 | deploymentId,
28 | projectName,
29 | statuses,
30 | pageToken,
31 | types,
32 | userAccessLevels,
33 | pageSize = 100,
34 | endTime,
35 | fields,
36 | prettyPrint = true
37 | }) => {
38 | const baseUrl = 'https://script.googleapis.com';
39 | const startTime_exec = Date.now();
40 |
41 | logger.info('API_CALL', 'Starting script processes list request', {
42 | scriptId,
43 | pageSize,
44 | startTime,
45 | endTime,
46 | functionName,
47 | deploymentId,
48 | baseUrl
49 | });
50 |
51 | try {
52 | // Validate required parameters
53 | if (!scriptId) {
54 | logger.error('API_CALL', 'Missing required parameter: scriptId');
55 | throw new Error('scriptId is required');
56 | }
57 |
58 | // Construct the URL with query parameters
59 | const url = new URL(`${baseUrl}/v1/processes`);
60 | const params = new URLSearchParams();
61 | params.append('userProcessFilter.scriptId', scriptId);
62 | if (startTime) params.append('userProcessFilter.startTime', startTime);
63 | if (functionName) params.append('userProcessFilter.functionName', functionName);
64 | if (deploymentId) params.append('userProcessFilter.deploymentId', deploymentId);
65 | if (projectName) params.append('userProcessFilter.projectName', projectName);
66 | if (statuses) params.append('userProcessFilter.statuses', statuses.join(','));
67 | if (pageToken) params.append('pageToken', pageToken);
68 | if (types) params.append('userProcessFilter.types', types.join(','));
69 | if (userAccessLevels) params.append('userProcessFilter.userAccessLevels', userAccessLevels.join(','));
70 | if (endTime) params.append('userProcessFilter.endTime', endTime);
71 | if (fields) params.append('fields', fields);
72 | params.append('pageSize', pageSize);
73 | params.append('prettyPrint', prettyPrint);
74 |
75 | url.search = params.toString();
76 |
77 | logger.debug('API_CALL', 'Constructed API URL', {
78 | url: url.toString(),
79 | queryParams: Object.fromEntries(params)
80 | });
81 |
82 | // Get OAuth headers
83 | logger.debug('API_CALL', 'Getting OAuth headers');
84 | const headers = await getAuthHeaders();
85 |
86 | logger.logAPICall('GET', url.toString(), headers);
87 |
88 | // Perform the fetch request
89 | const fetchStartTime = Date.now();
90 | const response = await fetch(url.toString(), {
91 | method: 'GET',
92 | headers
93 | });
94 |
95 | const fetchDuration = Date.now() - fetchStartTime;
96 | const responseSize = response.headers.get('content-length') || 'unknown';
97 |
98 | logger.logAPIResponse('GET', url.toString(), response.status, fetchDuration, responseSize);
99 |
100 | // Check if the response was successful
101 | if (!response.ok) {
102 | const errorText = await response.text();
103 | let errorData;
104 |
105 | try {
106 | errorData = JSON.parse(errorText);
107 | } catch (parseError) {
108 | errorData = { message: errorText };
109 | }
110 |
111 | logger.error('API_CALL', 'API request failed', {
112 | status: response.status,
113 | statusText: response.statusText,
114 | url: url.toString(),
115 | errorResponse: errorData,
116 | scriptId
117 | });
118 |
119 | throw new Error(`API Error (${response.status}): ${errorData.error?.message || errorData.message || 'Unknown error'}`);
120 | }
121 |
122 | // Parse and return the response data
123 | const data = await response.json();
124 | const totalDuration = Date.now() - startTime_exec;
125 |
126 | logger.info('API_CALL', 'Script processes list request completed successfully', {
127 | scriptId,
128 | processCount: data.processes ? data.processes.length : 0,
129 | hasNextPageToken: !!data.nextPageToken,
130 | totalDuration: `${totalDuration}ms`,
131 | responseSize: JSON.stringify(data).length
132 | });
133 |
134 | return data;
135 | } catch (error) {
136 | logger.error('API_CALL', 'Script processes list request failed', {
137 | scriptId,
138 | error: {
139 | message: error.message,
140 | stack: error.stack
141 | }
142 | });
143 |
144 | console.error('Error listing processes:', error);
145 | return {
146 | error: true,
147 | message: error.message,
148 | details: {
149 | scriptId,
150 | timestamp: new Date().toISOString(),
151 | errorType: error.name || 'Unknown'
152 | }
153 | };
154 | }
155 | };
156 |
157 | /**
158 | * Tool configuration for listing processes in Google Apps Script.
159 | * @type {Object}
160 | */
161 | const apiTool = {
162 | function: executeFunction,
163 | definition: {
164 | type: 'function',
165 | function: {
166 | name: 'script_processes_list',
167 | description: 'List processes for a Google Apps Script project.',
168 | parameters: {
169 | type: 'object',
170 | properties: {
171 | scriptId: {
172 | type: 'string',
173 | description: 'The ID of the script to filter processes.'
174 | },
175 | startTime: {
176 | type: 'string',
177 | description: 'The start time for filtering processes.'
178 | },
179 | functionName: {
180 | type: 'string',
181 | description: 'The name of the function to filter processes.'
182 | },
183 | deploymentId: {
184 | type: 'string',
185 | description: 'The deployment ID to filter processes.'
186 | },
187 | projectName: {
188 | type: 'string',
189 | description: 'The project name to filter processes.'
190 | },
191 | statuses: {
192 | type: 'array',
193 | items: {
194 | type: 'string'
195 | },
196 | description: 'The statuses to filter processes.'
197 | },
198 | pageToken: {
199 | type: 'string',
200 | description: 'Token for pagination.'
201 | },
202 | types: {
203 | type: 'array',
204 | items: {
205 | type: 'string'
206 | },
207 | description: 'The types of processes to filter.'
208 | },
209 | userAccessLevels: {
210 | type: 'array',
211 | items: {
212 | type: 'string'
213 | },
214 | description: 'User access levels to filter.'
215 | },
216 | pageSize: {
217 | type: 'integer',
218 | description: 'The number of processes to return per page.'
219 | },
220 | endTime: {
221 | type: 'string',
222 | description: 'The end time for filtering processes.'
223 | },
224 | fields: {
225 | type: 'string',
226 | description: 'Selector specifying which fields to include in a partial response.'
227 | },
228 | prettyPrint: {
229 | type: 'boolean',
230 | description: 'Returns response with indentations and line breaks.'
231 | }
232 | },
233 | required: ['scriptId']
234 | }
235 | }
236 | }
237 | };
238 |
239 | export { apiTool };
```
--------------------------------------------------------------------------------
/LOGGING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Enhanced MCP Server Logging
2 |
3 | This document describes the enhanced logging capabilities added to the MCP server for detailed monitoring and debugging of tool responses.
4 |
5 | ## Overview
6 |
7 | The MCP server now includes comprehensive logging that tracks:
8 | - Tool discovery and initialization
9 | - Tool execution requests and responses
10 | - API calls to Google Apps Script services
11 | - Authentication flows
12 | - Error conditions with detailed context
13 | - Performance metrics
14 |
15 | ## Configuration
16 |
17 | ### Log Levels
18 |
19 | Set the `LOG_LEVEL` environment variable in your `.env` file:
20 |
21 | ```env
22 | # LOG_LEVEL can be: error, warn, info, debug, trace
23 | LOG_LEVEL=info
24 | ```
25 |
26 | **Available Log Levels:**
27 | - `error`: Only error messages
28 | - `warn`: Errors and warnings
29 | - `info`: Errors, warnings, and informational messages (default)
30 | - `debug`: All above plus detailed debugging information
31 | - `trace`: Maximum verbosity (includes all internal operations)
32 |
33 | ### Environment Variables
34 |
35 | Add to your `.env` file:
36 | ```env
37 | # Logging Configuration
38 | LOG_LEVEL=info
39 | ```
40 |
41 | ## Features
42 |
43 | ### 1. Tool Execution Logging
44 |
45 | Every tool execution is logged with:
46 | - **Request ID**: Unique identifier for tracking
47 | - **Tool Name**: Which tool was called
48 | - **Arguments**: Input parameters (sanitized)
49 | - **Execution Time**: How long the tool took to execute
50 | - **Response Size**: Size of the response data
51 | - **Success/Failure Status**: Whether the tool completed successfully
52 |
53 | **Example Output:**
54 | ```
55 | [2025-06-01T10:30:15.123Z] [INFO] [TOOL_REQUEST] Executing tool: script_projects_get
56 | {
57 | "tool": "script_projects_get",
58 | "arguments": {
59 | "scriptId": "1BxKdN9XvlHF8rF9mF8..."
60 | },
61 | "requestId": "req_1717234215123_abc123def"
62 | }
63 |
64 | [2025-06-01T10:30:15.456Z] [INFO] [TOOL_RESPONSE] Tool completed: script_projects_get
65 | {
66 | "tool": "script_projects_get",
67 | "duration": "333ms",
68 | "requestId": "req_1717234215123_abc123def",
69 | "responseSize": 2048,
70 | "success": true
71 | }
72 | ```
73 |
74 | ### 2. API Call Logging
75 |
76 | All HTTP requests to Google APIs are logged with:
77 | - **Method and URL**: Complete request details
78 | - **Headers**: Sanitized headers (Authorization tokens are redacted)
79 | - **Response Status**: HTTP status codes
80 | - **Response Time**: Network latency
81 | - **Response Size**: Payload size
82 |
83 | **Example Output:**
84 | ```
85 | [2025-06-01T10:30:15.200Z] [DEBUG] [API_CALL] Making API request: GET https://script.googleapis.com/v1/projects/123
86 | {
87 | "method": "GET",
88 | "url": "https://script.googleapis.com/v1/projects/123",
89 | "headers": {
90 | "Authorization": "Bearer ***REDACTED***",
91 | "Accept": "application/json",
92 | "Content-Type": "application/json"
93 | }
94 | }
95 |
96 | [2025-06-01T10:30:15.400Z] [DEBUG] [API_RESPONSE] API response: GET https://script.googleapis.com/v1/projects/123
97 | {
98 | "method": "GET",
99 | "url": "https://script.googleapis.com/v1/projects/123",
100 | "status": 200,
101 | "responseTime": "200ms",
102 | "responseSize": "1024 bytes"
103 | }
104 | ```
105 |
106 | ### 3. Authentication Logging
107 |
108 | OAuth authentication flows are tracked:
109 | - **Token Requests**: When access tokens are requested
110 | - **Token Refresh**: When tokens are refreshed
111 | - **Authentication Failures**: Failed auth attempts with reasons
112 | - **Token Information**: Token types and scopes (sanitized)
113 |
114 | **Example Output:**
115 | ```
116 | [2025-06-01T10:30:14.500Z] [INFO] [AUTH] Requesting OAuth access token
117 | [2025-06-01T10:30:14.800Z] [INFO] [AUTH] Access token obtained successfully
118 | {
119 | "duration": "300ms",
120 | "tokenLength": 195,
121 | "tokenPrefix": "ya29.a0AWY7C..."
122 | }
123 | ```
124 |
125 | ### 4. Error Logging
126 |
127 | Comprehensive error tracking includes:
128 | - **Stack traces**: Full error call stacks
129 | - **Context information**: What was being attempted
130 | - **Request parameters**: Input data that caused the error
131 | - **Timing information**: How long the operation took before failing
132 |
133 | **Example Output:**
134 | ```
135 | [2025-06-01T10:30:16.123Z] [ERROR] [TOOL_ERROR] Tool failed: script_projects_get
136 | {
137 | "tool": "script_projects_get",
138 | "duration": "667ms",
139 | "requestId": "req_1717234215456_def456ghi",
140 | "error": {
141 | "message": "API Error (404): Script project not found",
142 | "stack": "Error: API Error (404): Script project not found\n at executeFunction...",
143 | "name": "Error"
144 | }
145 | }
146 | ```
147 |
148 | ### 5. Performance Metrics
149 |
150 | Track performance across all operations:
151 | - **Tool execution times**: How long each tool takes
152 | - **API response times**: Network latency to Google services
153 | - **Authentication times**: OAuth token acquisition time
154 | - **Overall request duration**: End-to-end request processing
155 |
156 | ## Usage
157 |
158 | ### 1. Running with Enhanced Logging
159 |
160 | ```bash
161 | # Start the MCP server with info level logging
162 | npm start
163 |
164 | # Start with debug logging for detailed information
165 | LOG_LEVEL=debug npm start
166 |
167 | # Start with minimal error-only logging
168 | LOG_LEVEL=error npm start
169 | ```
170 |
171 | ### 2. Testing the Logging
172 |
173 | Run the logging test script:
174 | ```bash
175 | node test-logging.js
176 | ```
177 |
178 | This will demonstrate all logging features and show you what to expect.
179 |
180 | ### 3. Log Output Examples
181 |
182 | **Startup Logs:**
183 | ```
184 | [2025-06-01T10:30:10.000Z] [INFO] [STARTUP] Starting MCP server in STDIO mode
185 | [2025-06-01T10:30:10.100Z] [INFO] [DISCOVERY] Starting tool discovery for 17 tool paths
186 | [2025-06-01T10:30:10.500Z] [INFO] [DISCOVERY] Tool discovery completed
187 | {
188 | "totalPaths": 17,
189 | "successfullyLoaded": 17,
190 | "failed": 0,
191 | "toolNames": ["script_projects_get", "script_projects_deployments_list", ...]
192 | }
193 | ```
194 |
195 | **Request Handling:**
196 | ```
197 | [2025-06-01T10:30:15.000Z] [INFO] [REQUEST] CallTool request received
198 | {
199 | "requestId": "req_1717234215000_xyz789",
200 | "toolName": "script_projects_get",
201 | "arguments": {
202 | "scriptId": "1BxKdN9XvlHF8rF9mF8..."
203 | },
204 | "timestamp": "2025-06-01T10:30:15.000Z"
205 | }
206 | ```
207 |
208 | ## Log Categories
209 |
210 | The logging system uses categories to organize different types of events:
211 |
212 | - **STARTUP**: Server initialization and configuration
213 | - **DISCOVERY**: Tool loading and discovery process
214 | - **REQUEST**: MCP protocol request handling
215 | - **EXECUTION**: Tool execution and results
216 | - **API_CALL**: HTTP requests to external APIs
217 | - **API_RESPONSE**: HTTP response processing
218 | - **AUTH**: Authentication and authorization
219 | - **ERROR**: Error conditions and failures
220 | - **TEST**: Testing and debugging operations
221 |
222 | ## Benefits
223 |
224 | 1. **Debugging**: Quickly identify issues with specific tools or API calls
225 | 2. **Performance Monitoring**: Track response times and identify bottlenecks
226 | 3. **Audit Trail**: Complete record of all operations for compliance
227 | 4. **Error Analysis**: Detailed error context for troubleshooting
228 | 5. **Usage Analytics**: Understand which tools are used most frequently
229 | 6. **Security Monitoring**: Track authentication events and failures
230 |
231 | ## Log File Integration
232 |
233 | While the current implementation outputs to console, you can easily redirect logs to files:
234 |
235 | ```bash
236 | # Log everything to a file
237 | node mcpServer.js 2>&1 | tee mcp-server.log
238 |
239 | # Log only errors to a separate file
240 | node mcpServer.js 2>error.log
241 |
242 | # Use with log rotation tools like logrotate
243 | node mcpServer.js 2>&1 | logger -t mcp-server
244 | ```
245 |
246 | ## Customization
247 |
248 | The logging system is built with the `MCPLogger` class in `lib/logger.js`. You can:
249 |
250 | 1. **Add new log categories**: Extend the logger for specific use cases
251 | 2. **Customize log format**: Modify the `formatMessage` method
252 | 3. **Add log filtering**: Implement custom filtering logic
253 | 4. **Integrate with external systems**: Send logs to monitoring services
254 |
255 | ## Security Considerations
256 |
257 | - **Token Redaction**: OAuth tokens are automatically redacted in logs
258 | - **Sensitive Data**: Personal data in request parameters should be sanitized
259 | - **Log Storage**: Ensure log files are properly secured if stored
260 | - **Retention**: Implement appropriate log retention policies
261 |
262 | ## Troubleshooting
263 |
264 | **Common Issues:**
265 |
266 | 1. **Too much logging**: Set `LOG_LEVEL=warn` or `LOG_LEVEL=error`
267 | 2. **Missing logs**: Check that `LOG_LEVEL` is set appropriately
268 | 3. **Performance impact**: Debug level logging may impact performance in production
269 |
270 | **Log Level Guidelines:**
271 | - **Production**: Use `info` or `warn` level
272 | - **Development**: Use `debug` level for detailed information
273 | - **Troubleshooting**: Use `trace` level for maximum detail
274 | - **Error monitoring**: Use `error` level for minimal output
275 |
```
--------------------------------------------------------------------------------
/mcpServer.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import dotenv from "dotenv";
4 | import express from "express";
5 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
8 | import {
9 | CallToolRequestSchema,
10 | ErrorCode,
11 | ListToolsRequestSchema,
12 | McpError,
13 | } from "@modelcontextprotocol/sdk/types.js";
14 | import { discoverTools } from "./lib/tools.js";
15 | import { logger } from "./lib/logger.js";
16 |
17 | import path from "path";
18 | import { fileURLToPath } from "url";
19 |
20 | const __filename = fileURLToPath(import.meta.url);
21 | const __dirname = path.dirname(__filename);
22 |
23 | dotenv.config({ path: path.resolve(__dirname, ".env") });
24 |
25 | const SERVER_NAME = "generated-mcp-server";
26 |
27 | async function transformTools(tools) {
28 | logger.info('SETUP', `Transforming ${tools.length} discovered tools`);
29 | const transformedTools = tools
30 | .map((tool) => {
31 | const definitionFunction = tool.definition?.function;
32 | if (!definitionFunction) {
33 | logger.warn('SETUP', `Tool missing definition function`, { toolPath: tool.path });
34 | return;
35 | }
36 | logger.debug('SETUP', `Transformed tool: ${definitionFunction.name}`, {
37 | name: definitionFunction.name,
38 | description: definitionFunction.description,
39 | requiredParams: definitionFunction.parameters?.required || []
40 | });
41 | return {
42 | name: definitionFunction.name,
43 | description: definitionFunction.description,
44 | inputSchema: definitionFunction.parameters,
45 | };
46 | })
47 | .filter(Boolean);
48 |
49 | logger.info('SETUP', `Successfully transformed ${transformedTools.length} tools`);
50 | return transformedTools;
51 | }
52 |
53 | async function setupServerHandlers(server, tools) {
54 | server.setRequestHandler(ListToolsRequestSchema, async () => {
55 | logger.info('REQUEST', 'Handling ListTools request');
56 | const transformedTools = await transformTools(tools);
57 | logger.info('REQUEST', `Returning ${transformedTools.length} available tools`, {
58 | toolNames: transformedTools.map(t => t.name)
59 | });
60 | return { tools: transformedTools };
61 | });
62 |
63 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
64 | const startTime = Date.now();
65 | const requestId = logger.generateRequestId();
66 | const toolName = request.params.name;
67 | const args = request.params.arguments;
68 |
69 | logger.info('REQUEST', `CallTool request received`, {
70 | requestId,
71 | toolName,
72 | arguments: args,
73 | timestamp: new Date().toISOString()
74 | });
75 |
76 | const tool = tools.find((t) => t.definition.function.name === toolName);
77 | if (!tool) {
78 | logger.error('REQUEST', `Tool not found: ${toolName}`, {
79 | requestId,
80 | availableTools: tools.map(t => t.definition.function.name)
81 | });
82 | throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
83 | }
84 |
85 | const requiredParameters = tool.definition?.function?.parameters?.required || [];
86 |
87 | // Validate required parameters
88 | for (const requiredParameter of requiredParameters) {
89 | if (!(requiredParameter in args)) {
90 | logger.error('REQUEST', `Missing required parameter`, {
91 | requestId,
92 | toolName,
93 | missingParameter: requiredParameter,
94 | providedArgs: Object.keys(args)
95 | });
96 | throw new McpError(
97 | ErrorCode.InvalidParams,
98 | `Missing required parameter: ${requiredParameter}`
99 | );
100 | }
101 | }
102 |
103 | try {
104 | logger.info('EXECUTION', `Executing tool: ${toolName}`, {
105 | requestId,
106 | toolPath: tool.path,
107 | validatedArgs: args
108 | });
109 |
110 | const result = await tool.function(args);
111 | const duration = Date.now() - startTime;
112 |
113 | logger.info('EXECUTION', `Tool execution completed successfully`, {
114 | requestId,
115 | toolName,
116 | duration: `${duration}ms`,
117 | resultType: typeof result,
118 | resultSize: JSON.stringify(result).length,
119 | hasError: !!result.error
120 | });
121 |
122 | // Log detailed result for debugging
123 | logger.debug('EXECUTION', `Tool result details`, {
124 | requestId,
125 | toolName,
126 | result: result
127 | });
128 |
129 | return {
130 | content: [
131 | {
132 | type: "text",
133 | text: JSON.stringify(result, null, 2),
134 | },
135 | ],
136 | };
137 | } catch (error) {
138 | const duration = Date.now() - startTime;
139 |
140 | logger.error('EXECUTION', `Tool execution failed`, {
141 | requestId,
142 | toolName,
143 | duration: `${duration}ms`,
144 | error: {
145 | message: error.message,
146 | stack: error.stack,
147 | name: error.name
148 | },
149 | args: args
150 | });
151 |
152 | console.error("[Error] Failed to fetch data:", error);
153 | throw new McpError(
154 | ErrorCode.InternalError,
155 | `API error: ${error.message}`
156 | );
157 | }
158 | });
159 | }
160 |
161 | async function run() {
162 | const args = process.argv.slice(2);
163 | const isSSE = args.includes("--sse");
164 |
165 | logger.info('STARTUP', `Starting MCP server in ${isSSE ? 'SSE' : 'STDIO'} mode`);
166 |
167 | const tools = await discoverTools();
168 | logger.info('STARTUP', `Discovered ${tools.length} tools`, {
169 | toolPaths: tools.map(t => t.path),
170 | toolNames: tools.map(t => t.definition?.function?.name).filter(Boolean)
171 | });
172 |
173 | if (isSSE) {
174 | const app = express();
175 | const transports = {};
176 | const servers = {};
177 |
178 | app.get("/sse", async (_req, res) => {
179 | const sessionId = Date.now().toString();
180 | logger.info('SSE', `New SSE connection established`, { sessionId });
181 |
182 | // Create a new Server instance for each session
183 | const server = new Server(
184 | {
185 | name: SERVER_NAME,
186 | version: "0.1.0",
187 | },
188 | {
189 | capabilities: {
190 | tools: {},
191 | },
192 | }
193 | );
194 |
195 | server.onerror = (error) => {
196 | logger.error('SSE', `Server error for session ${sessionId}`, error);
197 | console.error("[Error]", error);
198 | };
199 |
200 | await setupServerHandlers(server, tools);
201 |
202 | const transport = new SSEServerTransport("/messages", res);
203 | transports[transport.sessionId] = transport;
204 | servers[transport.sessionId] = server;
205 |
206 | res.on("close", async () => {
207 | logger.info('SSE', `SSE connection closed`, { sessionId: transport.sessionId });
208 | delete transports[transport.sessionId];
209 | await server.close();
210 | delete servers[transport.sessionId];
211 | });
212 |
213 | await server.connect(transport);
214 | });
215 |
216 | app.post("/messages", async (req, res) => {
217 | const sessionId = req.query.sessionId;
218 | const transport = transports[sessionId];
219 | const server = servers[sessionId];
220 |
221 | if (transport && server) {
222 | logger.debug('SSE', `Processing message for session`, { sessionId });
223 | await transport.handlePostMessage(req, res);
224 | } else {
225 | logger.warn('SSE', `No transport/server found for session`, { sessionId });
226 | res.status(400).send("No transport/server found for sessionId");
227 | }
228 | });
229 |
230 | const port = process.env.PORT || 3001;
231 | app.listen(port, () => {
232 | logger.info('SSE', `SSE server running on port ${port}`);
233 | console.log(`[SSE Server] running on port ${port}`);
234 | });
235 | } else {
236 | // stdio mode: single server instance
237 | logger.info('STDIO', 'Initializing STDIO server');
238 |
239 | const server = new Server(
240 | {
241 | name: SERVER_NAME,
242 | version: "0.1.0",
243 | },
244 | {
245 | capabilities: {
246 | tools: {},
247 | },
248 | }
249 | );
250 |
251 | server.onerror = (error) => {
252 | logger.error('STDIO', 'Server error', error);
253 | console.error("[Error]", error);
254 | };
255 |
256 | await setupServerHandlers(server, tools);
257 |
258 | process.on("SIGINT", async () => {
259 | logger.info('STDIO', 'Received SIGINT, shutting down gracefully');
260 | await server.close();
261 | process.exit(0);
262 | });
263 |
264 | const transport = new StdioServerTransport();
265 | logger.info('STDIO', 'STDIO server ready for connections');
266 | await server.connect(transport);
267 | }
268 | }
269 |
270 | run().catch(console.error);
271 |
```
--------------------------------------------------------------------------------
/test/test-complete-mcp-webapp.js:
--------------------------------------------------------------------------------
```javascript
1 | // Complete test script to create a web app deployment using MCP tools
2 | import { apiTool as updateContentTool } from './tools/google-app-script-api/apps-script-api/script-projects-update-content.js';
3 | import { apiTool as createVersionTool } from './tools/google-app-script-api/apps-script-api/script-projects-versions-create.js';
4 | import { apiTool as createDeploymentTool } from './tools/google-app-script-api/apps-script-api/script-projects-deployments-create.js';
5 | import { apiTool as getDeploymentTool } from './tools/google-app-script-api/apps-script-api/script-projects-deployments-get.js';
6 |
7 | const scriptId = '1fSY7y3Rh84FsgJmrFIMm4AUOV3mPgelLRvZ4Dahrv68zyDzX-cGbeYjn';
8 |
9 | async function createCompleteWebAppViaMCP() {
10 | console.log('🚀 Creating complete web app deployment via MCP tools...');
11 |
12 | try {
13 | // Step 1: Update script content with webapp configuration
14 | console.log('\n📝 Step 1: Updating script content with webapp configuration...');
15 |
16 | const updatedManifest = {
17 | "timeZone": "America/New_York",
18 | "dependencies": {},
19 | "exceptionLogging": "STACKDRIVER",
20 | "runtimeVersion": "V8",
21 | "webapp": {
22 | "access": "ANYONE",
23 | "executeAs": "USER_ACCESSING"
24 | }
25 | };
26 |
27 | const scriptContent = {
28 | files: [
29 | {
30 | name: "appsscript",
31 | type: "JSON",
32 | source: JSON.stringify(updatedManifest, null, 2)
33 | },
34 | {
35 | name: "code",
36 | type: "SERVER_JS",
37 | source: `/**
38 | * Serves the HTML page when the web app is accessed
39 | */
40 | function doGet() {
41 | return HtmlService.createHtmlOutputFromFile('index')
42 | .setTitle('Hello World App via MCP')
43 | .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
44 | }
45 |
46 | /**
47 | * Server-side function that can be called from the client
48 | */
49 | function getGreeting(name) {
50 | if (!name) {
51 | name = 'World';
52 | }
53 | return \`Hello, \${name}! This web app was created via MCP tools.\`;
54 | }
55 |
56 | /**
57 | * Get current time
58 | */
59 | function getCurrentTime() {
60 | return new Date().toLocaleString();
61 | }`
62 | },
63 | {
64 | name: "index",
65 | type: "HTML",
66 | source: `<!DOCTYPE html>
67 | <html>
68 | <head>
69 | <base target="_top">
70 | <title>Hello World App via MCP</title>
71 | <style>
72 | body {
73 | font-family: 'Google Sans', Roboto, Arial, sans-serif;
74 | max-width: 600px;
75 | margin: 50px auto;
76 | padding: 20px;
77 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
78 | color: white;
79 | text-align: center;
80 | }
81 | .container {
82 | background: rgba(255, 255, 255, 0.1);
83 | border-radius: 15px;
84 | padding: 30px;
85 | backdrop-filter: blur(10px);
86 | box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
87 | }
88 | h1 {
89 | color: #fff;
90 | margin-bottom: 30px;
91 | font-size: 2.5em;
92 | }
93 | input {
94 | padding: 12px;
95 | margin: 10px;
96 | border: none;
97 | border-radius: 8px;
98 | font-size: 16px;
99 | width: 250px;
100 | }
101 | button {
102 | background: #4285f4;
103 | color: white;
104 | border: none;
105 | padding: 12px 24px;
106 | margin: 10px;
107 | border-radius: 8px;
108 | cursor: pointer;
109 | font-size: 16px;
110 | transition: background 0.3s;
111 | }
112 | button:hover {
113 | background: #3367d6;
114 | }
115 | .result {
116 | margin-top: 20px;
117 | padding: 20px;
118 | background: rgba(255, 255, 255, 0.2);
119 | border-radius: 8px;
120 | min-height: 50px;
121 | display: flex;
122 | align-items: center;
123 | justify-content: center;
124 | }
125 | .loading {
126 | display: none;
127 | color: #ffd700;
128 | }
129 | .timestamp {
130 | margin-top: 10px;
131 | font-size: 0.9em;
132 | opacity: 0.8;
133 | }
134 | </style>
135 | </head>
136 | <body>
137 | <div class="container">
138 | <h1>🌟 Hello World App via MCP 🌟</h1>
139 | <p>This web app was created using MCP (Model Context Protocol) tools!</p>
140 |
141 | <div class="input-group">
142 | <input type="text" id="nameInput" placeholder="Enter your name (optional)" />
143 | </div>
144 |
145 | <div>
146 | <button onclick="sayHello()">Say Hello</button>
147 | <button onclick="getTime()">Get Current Time</button>
148 | </div>
149 |
150 | <div id="result" class="result">
151 | Click a button to see the MCP magic! ✨
152 | </div>
153 |
154 | <div id="loading" class="loading">Loading...</div>
155 |
156 | <div id="timestamp" class="timestamp"></div>
157 | </div>
158 |
159 | <script>
160 | function sayHello() {
161 | showLoading();
162 | const name = document.getElementById('nameInput').value;
163 |
164 | google.script.run
165 | .withSuccessHandler(onSuccess)
166 | .withFailureHandler(onFailure)
167 | .getGreeting(name);
168 | }
169 |
170 | function getTime() {
171 | showLoading();
172 |
173 | google.script.run
174 | .withSuccessHandler(onTimeSuccess)
175 | .withFailureHandler(onFailure)
176 | .getCurrentTime();
177 | }
178 |
179 | function onSuccess(result) {
180 | hideLoading();
181 | document.getElementById('result').innerHTML = \`
182 | <div style="font-size: 20px; font-weight: bold;">
183 | \${result}
184 | </div>
185 | \`;
186 | updateTimestamp();
187 | }
188 |
189 | function onTimeSuccess(result) {
190 | hideLoading();
191 | document.getElementById('result').innerHTML = \`
192 | <div style="font-size: 18px;">
193 | 🕒 Current Time: <strong>\${result}</strong>
194 | </div>
195 | \`;
196 | updateTimestamp();
197 | }
198 |
199 | function onFailure(error) {
200 | hideLoading();
201 | document.getElementById('result').innerHTML = \`
202 | <div style="color: #ff6b6b;">
203 | ❌ Error: \${error.message || 'Something went wrong!'}
204 | </div>
205 | \`;
206 | updateTimestamp();
207 | }
208 |
209 | function showLoading() {
210 | document.getElementById('loading').style.display = 'block';
211 | document.getElementById('result').style.display = 'none';
212 | }
213 |
214 | function hideLoading() {
215 | document.getElementById('loading').style.display = 'none';
216 | document.getElementById('result').style.display = 'flex';
217 | }
218 |
219 | function updateTimestamp() {
220 | document.getElementById('timestamp').textContent =
221 | 'Last updated: ' + new Date().toLocaleTimeString();
222 | }
223 |
224 | // Initial timestamp
225 | updateTimestamp();
226 | </script>
227 | </body>
228 | </html>`
229 | }
230 | ]
231 | };
232 |
233 | const updateResult = await updateContentTool.function({
234 | scriptId: scriptId,
235 | files: scriptContent.files
236 | });
237 |
238 | console.log('✅ Script content updated:', JSON.stringify(updateResult, null, 2));
239 |
240 | // Step 2: Create a new version
241 | console.log('\n📦 Step 2: Creating new version...');
242 |
243 | const versionResult = await createVersionTool.function({
244 | scriptId: scriptId,
245 | description: 'Web app version created via MCP tools'
246 | });
247 |
248 | console.log('✅ Version created:', JSON.stringify(versionResult, null, 2));
249 |
250 | if (!versionResult.versionNumber) {
251 | throw new Error('Failed to create version');
252 | }
253 |
254 | // Step 3: Create deployment
255 | console.log('\n🚀 Step 3: Creating deployment...');
256 |
257 | const deploymentResult = await createDeploymentTool.function({
258 | scriptId: scriptId,
259 | manifestFileName: 'appsscript',
260 | versionNumber: versionResult.versionNumber,
261 | description: 'Web app deployment via MCP tools'
262 | });
263 |
264 | console.log('✅ Deployment created:', JSON.stringify(deploymentResult, null, 2));
265 |
266 | if (!deploymentResult.deploymentId) {
267 | throw new Error('Failed to create deployment');
268 | }
269 |
270 | // Step 4: Get deployment details with entry points
271 | console.log('\n🔍 Step 4: Getting deployment details...');
272 |
273 | const deploymentDetails = await getDeploymentTool.function({
274 | scriptId: scriptId,
275 | deploymentId: deploymentResult.deploymentId
276 | });
277 |
278 | console.log('✅ Deployment details:', JSON.stringify(deploymentDetails, null, 2));
279 |
280 | // Check for web app URL
281 | if (deploymentDetails.entryPoints && deploymentDetails.entryPoints.length > 0) {
282 | console.log('\n🎉 SUCCESS! Web app deployment completed!');
283 | console.log('📱 Deployment ID:', deploymentResult.deploymentId);
284 |
285 | const webAppEntry = deploymentDetails.entryPoints.find(entry => entry.entryPointType === 'WEB_APP');
286 | if (webAppEntry) {
287 | console.log('🌐 Web App URL:', webAppEntry.webApp.url);
288 | console.log('🔒 Access Level:', webAppEntry.webApp.access);
289 | console.log('👤 Execute As:', webAppEntry.webApp.executeAs);
290 | console.log('\n🚀 Your web app is now live and accessible to anyone!');
291 | }
292 | } else {
293 | console.log('⚠️ Deployment created but no entry points found. May need to wait for propagation.');
294 | }
295 |
296 | } catch (error) {
297 | console.error('💥 Error during web app creation:', error);
298 | }
299 | }
300 |
301 | createCompleteWebAppViaMCP();
302 |
```
--------------------------------------------------------------------------------
/test/oauth-setup-broken.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * OAuth Setup Script for Google Apps Script API
5 | * This script helps you obtain and securely store OAuth tokens
6 | */
7 |
8 | import 'dotenv/config';
9 | import { manualOAuthFlow } from '../lib/oauth-helper.js';
10 | import { TokenManager } from '../lib/tokenManager.js';
11 | import { readFileSync } from 'fs';
12 |
13 | console.log('🔐 Google Apps Script API OAuth Setup');
14 | console.log('=====================================\n');
15 |
16 | async function setupOAuth() {
17 | console.log('📋 This script will help you set up OAuth authentication for Google Apps Script API.');
18 | console.log('📝 You need to have your CLIENT_ID and CLIENT_SECRET configured in .env file.\n');
19 |
20 | const tokenManager = new TokenManager();
21 |
22 | // Handle info command
23 | if (process.argv.includes('--info')) {
24 | const tokenInfo = tokenManager.getTokenInfo();
25 |
26 | console.log('🔍 Token Information:');
27 | console.log('=====================\n');
28 |
29 | if (tokenInfo.hasTokens) {
30 | console.log('✅ Tokens found');
31 | console.log(`📁 Location: ${tokenInfo.location}`);
32 | console.log(`💾 Saved at: ${tokenInfo.savedAt}`);
33 | console.log(`⏰ Expires at: ${tokenInfo.expiresAt}`);
34 | console.log(`📊 Status: ${tokenInfo.status}`);
35 | console.log(`🔐 Scope: ${tokenInfo.scope || 'Not specified'}`);
36 | } else {
37 | console.log('❌ No tokens found');
38 | console.log(`📁 Expected location: ${tokenInfo.location}`);
39 | console.log('\n💡 Run "node oauth-setup.js" to set up OAuth tokens');
40 | }
41 |
42 | process.exit(0);
43 | }
44 |
45 | // Check if tokens already exist
46 | const tokenInfo = tokenManager.getTokenInfo();
47 | if (tokenInfo.hasTokens) {
48 | console.log('🔍 Found existing tokens:');
49 | console.log(` 📁 Location: ${tokenInfo.location}`);
50 | console.log(` 💾 Saved at: ${tokenInfo.savedAt}`);
51 | console.log(` ⏰ Expires at: ${tokenInfo.expiresAt}`);
52 | console.log(` 📊 Status: ${tokenInfo.status}`);
53 | console.log(` 🔐 Scope: ${tokenInfo.scope || 'Not specified'}\n`);
54 |
55 | if (!tokenInfo.isExpired) {
56 | console.log('✅ You already have valid tokens stored.');
57 | console.log('💡 To get new tokens, run: node oauth-setup.js --force');
58 | console.log('🗑️ To clear existing tokens, run: node oauth-setup.js --clear\n');
59 |
60 | if (!process.argv.includes('--force')) {
61 | process.exit(0);
62 | }
63 | }
64 | }
65 |
66 | // Handle clear command
67 | if (process.argv.includes('--clear')) {
68 | tokenManager.clearTokens();
69 | console.log('✅ Tokens cleared successfully.');
70 | process.exit(0);
71 | }
72 |
73 | try {
74 | // Check if .env file exists and has required credentials
75 | const envPath = '.env';
76 | let envContent = '';
77 |
78 | try {
79 | envContent = readFileSync(envPath, 'utf8');
80 | console.log('✅ Found .env file');
81 | } catch (error) {
82 | console.error('❌ No .env file found. Please create one first with your CLIENT_ID and CLIENT_SECRET.');
83 | console.log('\n📝 Example .env file content:');
84 | console.log('GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_client_id_here');
85 | console.log('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_client_secret_here');
86 | console.log('\n📖 Note: Refresh token is now stored securely and not needed in .env file');
87 | process.exit(1);
88 | }
89 | // Check for required credentials
90 | const hasClientId = envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_ID=') &&
91 | !envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_client_id_here');
92 | const hasClientSecret = envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=') &&
93 | !envContent.includes('GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_client_secret_here');
94 |
95 | if (!hasClientId || !hasClientSecret) {
96 | console.error('❌ Missing CLIENT_ID or CLIENT_SECRET in .env file.');
97 | console.log('\n🔧 Please update your .env file with valid credentials:');
98 | console.log(' - GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_actual_client_id');
99 | console.log(' - GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_actual_client_secret');
100 | console.log('\n📖 See OAUTH_SETUP.md for instructions on obtaining these credentials.');
101 | process.exit(1);
102 | }
103 |
104 | console.log('✅ Found required credentials in .env file'); console.log('\n🚀 Starting OAuth flow...');
105 | console.log('📱 Your browser will open automatically');
106 | console.log('🔐 Please authorize the application when prompted');
107 | console.log('⏳ Waiting for authorization...\n');
108 |
109 | // Start OAuth flow
110 | const tokens = await manualOAuthFlow();
111 |
112 | if (tokens.refresh_token) {
113 | console.log('\n🎉 OAuth setup successful!');
114 | console.log('🔑 Access token obtained:', tokens.access_token ? '✅' : '❌');
115 | console.log('🔄 Refresh token obtained:', tokens.refresh_token ? '✅' : '❌');
116 |
117 | // Save tokens securely using TokenManager
118 | try {
119 | tokenManager.saveTokens(tokens);
120 | console.log('💾 Tokens saved securely');
121 |
122 | const tokenInfo = tokenManager.getTokenInfo();
123 | console.log(`📁 Token location: ${tokenInfo.location}`);
124 |
125 | } catch (error) {
126 | console.error('❌ Failed to save tokens:', error.message);
127 | console.log('\n📝 Please run the setup again or contact support.');
128 | process.exit(1);
129 | }
130 |
131 | console.log('\n📋 Setup Summary:');
132 | console.log(' ✅ OAuth flow completed');
133 | console.log(' ✅ Access token obtained');
134 | console.log(' ✅ Refresh token obtained');
135 | console.log(' ✅ Tokens stored securely');
136 | console.log('\n🔐 Security Notes:');
137 | console.log(' 🔒 Refresh token is stored with restricted file permissions');
138 | console.log(' ⏰ Access token will be refreshed automatically');
139 | console.log(' 🚫 No sensitive tokens are stored in .env file');
140 | console.log('\n🎯 Next Steps:');
141 | console.log(' 1. Test the OAuth setup: npm run test-oauth');
142 | console.log(' 2. Configure your MCP client (Claude Desktop, VS Code, etc.)');
143 | console.log(' 3. Use your MCP tools with confidence!');
144 |
145 | } else {
146 | console.error('\n❌ OAuth setup failed: No refresh token received');
147 | console.log('🔧 This might happen if:');
148 | console.log(' - Your OAuth app is not configured correctly');
149 | console.log(' - You denied the authorization request');
150 | console.log(' - There was a network error during the process');
151 | console.log('\n📖 Please check the OAUTH_SETUP.md guide and try again.');
152 | process.exit(1);
153 | }
154 |
155 | } catch (error) {
156 | console.error('\n❌ OAuth setup failed:', error.message);
157 |
158 | if (error.message.includes('EADDRINUSE')) {
159 | console.log('\n🔧 Port already in use. Please:');
160 | console.log(' 1. Close any other applications using port 3001');
161 | console.log(' 2. Wait a moment and try again');
162 | } else if (error.message.includes('CLIENT_ID') || error.message.includes('CLIENT_SECRET')) {
163 | console.log('\n🔧 OAuth credential issue. Please:');
164 | console.log(' 1. Check your .env file has correct credentials');
165 | console.log(' 2. Verify credentials in Google Cloud Console');
166 | console.log(' 3. Make sure OAuth consent screen is configured');
167 | } else {
168 | console.log('\n🔧 Please check:');
169 | console.log(' 1. Your internet connection');
170 | console.log(' 2. Google Cloud Console OAuth configuration');
171 | console.log(' 3. That you authorized the application in the browser');
172 | }
173 |
174 | console.log('\n📖 For detailed setup instructions, see OAUTH_SETUP.md');
175 | process.exit(1);
176 | }
177 | }
178 |
179 | // Handle command line arguments
180 | if (process.argv.includes('--help') || process.argv.includes('-h')) {
181 | console.log('📖 Google Apps Script OAuth Setup');
182 | console.log('\nUsage:');
183 | console.log(' node oauth-setup.js # Run OAuth setup');
184 | console.log(' node oauth-setup.js --force # Force new OAuth setup (overwrite existing tokens)');
185 | console.log(' node oauth-setup.js --clear # Clear stored tokens');
186 | console.log(' node oauth-setup.js --info # Show token information');
187 | console.log(' node oauth-setup.js --help # Show this help');
188 | process.exit(0);
189 | }
190 |
191 | if (process.argv.includes('--info')) {
192 | const tokenManager = new TokenManager();
193 | const tokenInfo = tokenManager.getTokenInfo();
194 |
195 | console.log('🔍 Token Information:');
196 | console.log('=====================\n');
197 |
198 | if (tokenInfo.hasTokens) {
199 | console.log('✅ Tokens found');
200 | console.log(`📁 Location: ${tokenInfo.location}`);
201 | console.log(`💾 Saved at: ${tokenInfo.savedAt}`);
202 | console.log(`⏰ Expires at: ${tokenInfo.expiresAt}`);
203 | console.log(`📊 Status: ${tokenInfo.status}`);
204 | console.log(`🔐 Scope: ${tokenInfo.scope || 'Not specified'}`);
205 | } else {
206 | console.log('❌ No tokens found');
207 | console.log(`📁 Expected location: ${tokenInfo.location}`);
208 | console.log('\n💡 Run "node oauth-setup.js" to set up OAuth tokens');
209 | }
210 |
211 | process.exit(0);
212 | }
213 |
214 | // Run setup
215 | setupOAuth().catch((error) => {
216 | console.error('💥 Unexpected error:', error);
217 | process.exit(1);
218 | });
219 |
220 | } else {
221 | console.log('\n⚠️ OAuth completed but no refresh token received.');
222 | console.log('🔄 You may need to revoke and re-authorize the application.');
223 | console.log('📖 Check the Google Cloud Console for your OAuth settings.');
224 | }
225 |
226 | } catch (error) {
227 | console.error('\n❌ OAuth setup failed:', error.message);
228 | console.log('\n🔧 Troubleshooting:');
229 | console.log(' 1. Check your internet connection');
230 | console.log(' 2. Verify your CLIENT_ID and CLIENT_SECRET are correct');
231 | console.log(' 3. Ensure the redirect URI is registered in Google Cloud Console');
232 | console.log(' 4. Make sure Google Apps Script API is enabled');
233 | console.log(' 5. Try revoking and re-creating your OAuth credentials');
234 | console.log('\n📖 For detailed setup instructions, see OAUTH_SETUP.md');
235 | process.exit(1);
236 | }
237 | }
238 |
239 | // Run setup if this script is executed directly
240 | console.log('🔍 Debug: process.argv[1]:', process.argv[1]);
241 | console.log('🔍 Debug: endsWith check:', process.argv[1] && process.argv[1].endsWith('oauth-setup.js'));
242 |
243 | if (process.argv[1] && process.argv[1].endsWith('oauth-setup.js')) {
244 | console.log('🚀 Starting OAuth setup...');
245 | setupOAuth();
246 | } else {
247 | console.log('❌ Script not executed directly, skipping setup');
248 | }
249 |
```
--------------------------------------------------------------------------------
/OAUTH_SETUP.md:
--------------------------------------------------------------------------------
```markdown
1 | # Google Apps Script API OAuth Setup Guide
2 |
3 | This guide will help you set up OAuth authentication for the Google Apps Script API MCP server.
4 |
5 | ## Prerequisites
6 |
7 | 1. **Google Cloud Project**: You need a Google Cloud Project with Google Apps Script API enabled
8 | 2. **OAuth 2.0 Credentials**: You need Client ID and Client Secret from Google Cloud Console
9 | 3. **Node.js**: Make sure you have Node.js 16+ installed
10 |
11 | ## Step 1: Google Cloud Console Setup
12 |
13 | ### 1.1 Create/Select a Project
14 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
15 | 2. Create a new project or select an existing one
16 | 3. Note your project ID
17 |
18 | ### 1.2 Enable Google Apps Script API
19 | 1. In the Google Cloud Console, go to **APIs & Services** > **Library**
20 | 2. Search for "Google Apps Script API"
21 | 3. Click on it and press **Enable**
22 |
23 | ### 1.3 Create OAuth 2.0 Credentials
24 | 1. Go to **APIs & Services** > **Credentials**
25 | 2. Click **+ CREATE CREDENTIALS** > **OAuth 2.0 Client IDs**
26 | 3. If prompted, configure the OAuth consent screen:
27 | - Choose **External** (unless you're in a Google Workspace organization)
28 | - Fill in the required fields:
29 | - App name: "Google Apps Script MCP Server"
30 | - User support email: Your email
31 | - Developer contact information: Your email
32 | - Add scopes (optional for testing):
33 | - `https://www.googleapis.com/auth/script.projects`
34 | - `https://www.googleapis.com/auth/script.projects.readonly`
35 | - `https://www.googleapis.com/auth/script.deployments.readonly`
36 | - `https://www.googleapis.com/auth/script.metrics`
37 | 4. For Application Type, choose **Web application**
38 | 5. Add authorized redirect URIs:
39 | - `http://localhost:3001/oauth/callback`
40 | 6. Click **Create**
41 | 7. Copy your **Client ID** and **Client Secret**
42 |
43 | ## Step 2: Configure Environment Variables
44 |
45 | ### 2.1 Update .env File
46 | Edit the `.env` file in your project root and add your credentials:
47 |
48 | ```env
49 | # Google Apps Script API OAuth Configuration
50 | GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_client_id_here
51 | GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_client_secret_here
52 | GOOGLE_APP_SCRIPT_API_REFRESH_TOKEN=your_refresh_token_here
53 |
54 | # OAuth Configuration
55 | GOOGLE_APP_SCRIPT_API_REDIRECT_URI=http://localhost:3001/oauth/callback
56 | ```
57 |
58 | Replace:
59 | - `your_client_id_here` with your actual Client ID
60 | - `your_client_secret_here` with your actual Client Secret
61 | - Keep `your_refresh_token_here` as is (we'll get this in the next step)
62 |
63 | ## Step 3: Run OAuth Setup
64 |
65 | ### 3.1 Install Dependencies
66 | ```bash
67 | npm install
68 | ```
69 |
70 | ### 3.2 Run OAuth Setup Script
71 | ```bash
72 | npm run setup-oauth
73 | ```
74 |
75 | This script will:
76 | 1. Validate your credentials
77 | 2. Open your browser for OAuth authorization
78 | 3. Handle the OAuth callback
79 | 4. Automatically update your `.env` file with the refresh token
80 |
81 | ### 3.3 Authorize the Application
82 | 1. Your browser will open automatically
83 | 2. Sign in to your Google account
84 | 3. Review and accept the permissions
85 | 4. The browser will show a success message
86 | 5. Return to your terminal - the setup should be complete
87 |
88 | ## Step 4: Test Your Setup
89 |
90 | ### 4.1 Run OAuth Test
91 | ```bash
92 | npm run test-oauth
93 | ```
94 |
95 | This will verify that your OAuth setup is working correctly.
96 |
97 | ### 4.2 Test with MCP Client
98 | You can now use your MCP tools with any MCP-compatible client like:
99 | - Claude Desktop
100 | - Postman
101 | - Other MCP clients
102 |
103 | ## Troubleshooting
104 |
105 | ### Common Issues
106 |
107 | #### 1. "redirect_uri_mismatch" Error
108 | - Make sure you added `http://localhost:3001/oauth/callback` to your authorized redirect URIs in Google Cloud Console
109 | - Check that the URI is exactly correct (no trailing slash, correct port)
110 |
111 | #### 2. "access_denied" Error
112 | - Make sure you're signed in to the correct Google account
113 | - Check that the OAuth consent screen is properly configured
114 | - Verify that the Google Apps Script API is enabled
115 |
116 | #### 3. "invalid_client" Error
117 | - Double-check your Client ID and Client Secret in the `.env` file
118 | - Make sure there are no extra spaces or quotes around the values
119 |
120 | #### 4. Port Already in Use
121 | - Make sure port 3001 is not being used by another application
122 | - If needed, you can modify the port in `lib/oauth-helper.js` and update your redirect URI accordingly
123 |
124 | #### 5. Refresh Token Not Saved
125 | - Make sure the OAuth setup script has write permissions to the `.env` file
126 | - Check that the `.env` file exists and is in the correct location
127 |
128 | ### Advanced Configuration
129 |
130 | #### Custom Port
131 | If you need to use a different port, update these files:
132 | 1. `lib/oauth-helper.js` - Change the `PORT` constant
133 | 2. `.env` - Update `GOOGLE_APP_SCRIPT_API_REDIRECT_URI`
134 | 3. Google Cloud Console - Update the authorized redirect URI
135 |
136 | #### Custom Scopes
137 | To modify the required permissions, edit the `SCOPES` array in `lib/oauth-helper.js`.
138 |
139 | Available scopes:
140 | - `https://www.googleapis.com/auth/script.projects` - Read/write access to Apps Script projects
141 | - `https://www.googleapis.com/auth/script.projects.readonly` - Read-only access to Apps Script projects
142 | - `https://www.googleapis.com/auth/script.deployments.readonly` - Read access to Apps Script deployments
143 | - `https://www.googleapis.com/auth/script.metrics` - Access to Apps Script execution metrics
144 |
145 | ## Security Notes
146 |
147 | 1. **Keep your credentials secure**: Never commit your `.env` file to version control
148 | 2. **Refresh token**: The refresh token allows long-term access - treat it like a password
149 | 3. **Client secret**: Keep your client secret confidential
150 | 4. **Production use**: For production, consider using more secure token storage than environment variables
151 |
152 | ## Support
153 |
154 | If you encounter issues:
155 | 1. Check the troubleshooting section above
156 | 2. Review the Google Apps Script API documentation
157 | 3. Check Google Cloud Console for any error messages
158 | 4. Ensure all prerequisites are met
159 |
160 | For more information about Google Apps Script API:
161 | - [Google Apps Script API Documentation](https://developers.google.com/apps-script/api)
162 | - [OAuth 2.0 for Web Server Applications](https://developers.google.com/identity/protocols/oauth2/web-server)
163 | 2. Google Apps Script API enabled
164 | 3. OAuth 2.0 credentials configured
165 |
166 | ## Step 1: Create OAuth 2.0 Credentials
167 |
168 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
169 | 2. Select your project (or create a new one)
170 | 3. Navigate to **APIs & Services** > **Credentials**
171 | 4. Click **+ CREATE CREDENTIALS** > **OAuth client ID**
172 | 5. Select **Desktop application** as the application type
173 | 6. Give it a name (e.g., "Google Apps Script MCP Client")
174 | 7. Click **Create**
175 | 8. Download the JSON file containing your credentials
176 |
177 | ## Step 2: Enable Required APIs
178 |
179 | Make sure the following APIs are enabled in your GCP project:
180 |
181 | 1. Google Apps Script API
182 | 2. Google Drive API (if accessing script files)
183 |
184 | To enable APIs:
185 | 1. Go to **APIs & Services** > **Library**
186 | 2. Search for each API and click **Enable**
187 |
188 | ## Step 3: Get a Refresh Token
189 |
190 | You'll need to obtain a refresh token through the OAuth 2.0 flow. Here's a simple way to do it:
191 |
192 | ### Option A: Using Google OAuth 2.0 Playground
193 |
194 | 1. Go to [Google OAuth 2.0 Playground](https://developers.google.com/oauthplayground/)
195 | 2. Click the gear icon (⚙️) in the top right
196 | 3. Check **Use your own OAuth credentials**
197 | 4. Enter your **OAuth Client ID** and **OAuth Client secret**
198 | 5. In the left panel, find and select **Google Apps Script API v1**
199 | 6. Select the scope: `https://www.googleapis.com/auth/script.projects`
200 | 7. Click **Authorize APIs**
201 | 8. Complete the authorization flow
202 | 9. Click **Exchange authorization code for tokens**
203 | 10. Copy the **Refresh token** from the response
204 |
205 | ### Option B: Using curl (Advanced)
206 |
207 | ```bash
208 | # Step 1: Get authorization code (open this URL in browser)
209 | https://accounts.google.com/o/oauth2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/script.projects&response_type=code
210 |
211 | # Step 2: Exchange code for tokens
212 | curl -X POST https://oauth2.googleapis.com/token \
213 | -H "Content-Type: application/x-www-form-urlencoded" \
214 | -d "client_id=YOUR_CLIENT_ID" \
215 | -d "client_secret=YOUR_CLIENT_SECRET" \
216 | -d "code=AUTHORIZATION_CODE_FROM_STEP_1" \
217 | -d "grant_type=authorization_code" \
218 | -d "redirect_uri=urn:ietf:wg:oauth:2.0:oob"
219 | ```
220 |
221 | ## Step 4: Configure Environment Variables
222 |
223 | Update your `.env` file with the OAuth credentials:
224 |
225 | ```env
226 | # Google Apps Script API OAuth Configuration
227 | GOOGLE_APP_SCRIPT_API_CLIENT_ID=your_client_id_here
228 | GOOGLE_APP_SCRIPT_API_CLIENT_SECRET=your_client_secret_here
229 | GOOGLE_APP_SCRIPT_API_REFRESH_TOKEN=your_refresh_token_here
230 | ```
231 |
232 | Replace the placeholder values with:
233 | - `your_client_id_here`: Your OAuth 2.0 Client ID
234 | - `your_client_secret_here`: Your OAuth 2.0 Client Secret
235 | - `your_refresh_token_here`: The refresh token obtained in Step 3
236 |
237 | ## Required Scopes
238 |
239 | The following OAuth scopes are required for different operations:
240 |
241 | - `https://www.googleapis.com/auth/script.projects` - Manage Google Apps Script projects
242 | - `https://www.googleapis.com/auth/script.processes` - View Google Apps Script processes
243 | - `https://www.googleapis.com/auth/script.deployments` - Manage deployments
244 | - `https://www.googleapis.com/auth/drive` - Access Drive files (if needed)
245 |
246 | ## Security Notes
247 |
248 | 1. **Keep credentials secure**: Never commit your `.env` file to version control
249 | 2. **Refresh token rotation**: Google may rotate refresh tokens periodically
250 | 3. **Access token expiry**: Access tokens typically expire after 1 hour
251 | 4. **Scope principle**: Only request the minimum scopes needed for your application
252 |
253 | ## Troubleshooting
254 |
255 | ### Common Issues
256 |
257 | 1. **"Invalid credentials"**: Check that your client ID and secret are correct
258 | 2. **"Invalid scope"**: Ensure the required APIs are enabled in your GCP project
259 | 3. **"Refresh token expired"**: You may need to re-authorize and get a new refresh token
260 | 4. **"Project not found"**: Make sure the script project exists and you have access to it
261 |
262 | ### Testing Authentication
263 |
264 | You can test your OAuth setup by running a simple API call:
265 |
266 | ```javascript
267 | import { getOAuthAccessToken } from './lib/oauth-helper.js';
268 |
269 | async function testAuth() {
270 | try {
271 | const token = await getOAuthAccessToken();
272 | console.log('OAuth authentication successful!');
273 | console.log('Access token received:', token.substring(0, 20) + '...');
274 | } catch (error) {
275 | console.error('OAuth authentication failed:', error.message);
276 | }
277 | }
278 |
279 | testAuth();
280 | ```
281 |
282 | ## Migration from API Key
283 |
284 | If you were previously using an API key, the OAuth implementation provides:
285 |
286 | 1. **Better security**: OAuth tokens are time-limited and can be revoked
287 | 2. **Fine-grained access**: Specific scopes control what the application can access
288 | 3. **User context**: Operations are performed in the context of the authenticated user
289 | 4. **Compliance**: Meets Google's authentication requirements for sensitive APIs
290 |
291 | The OAuth helper automatically handles token refresh, so your application will continue working even after access tokens expire.
292 |
```
--------------------------------------------------------------------------------
/test/deploy-complete-webapp.js:
--------------------------------------------------------------------------------
```javascript
1 | import { getOAuthAccessToken } from '../lib/oauth-helper.js';
2 |
3 | const scriptId = '1fSY7y3Rh84FsgJmrFIMm4AUOV3mPgelLRvZ4Dahrv68zyDzX-cGbeYjn';
4 |
5 | async function updateScriptWithWebAppConfig() {
6 | try {
7 | const token = await getOAuthAccessToken();
8 |
9 | console.log('Updating script with web app configuration...');
10 |
11 | // Updated manifest with web app configuration
12 | const updatedManifest = {
13 | "timeZone": "America/New_York",
14 | "dependencies": {},
15 | "exceptionLogging": "STACKDRIVER",
16 | "runtimeVersion": "V8",
17 | "webapp": {
18 | "access": "ANYONE",
19 | "executeAs": "USER_ACCESSING"
20 | }
21 | };
22 |
23 | // Prepare the updated script content
24 | const scriptContent = {
25 | files: [
26 | {
27 | name: "appsscript",
28 | type: "JSON",
29 | source: JSON.stringify(updatedManifest, null, 2)
30 | },
31 | {
32 | name: "code",
33 | type: "SERVER_JS",
34 | source: `/**
35 | * Serves the HTML page when the web app is accessed
36 | */
37 | function doGet() {
38 | return HtmlService.createHtmlOutputFromFile('index')
39 | .setTitle('Hello World App')
40 | .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
41 | }
42 |
43 | /**
44 | * Server-side function that can be called from the client
45 | */
46 | function getGreeting(name) {
47 | if (!name) {
48 | name = 'World';
49 | }
50 | return \`Hello, \${name}! This is a Google Apps Script web app.\`;
51 | }
52 |
53 | /**
54 | * Function to get current timestamp
55 | */
56 | function getCurrentTime() {
57 | return new Date().toLocaleString();
58 | }`
59 | },
60 | {
61 | name: "index",
62 | type: "HTML",
63 | source: `<!DOCTYPE html>
64 | <html>
65 | <head>
66 | <meta charset="utf-8">
67 | <meta name="viewport" content="width=device-width, initial-scale=1">
68 | <title>Hello World - Google Apps Script</title>
69 | <style>
70 | body {
71 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
72 | max-width: 800px;
73 | margin: 0 auto;
74 | padding: 20px;
75 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
76 | min-height: 100vh;
77 | color: white;
78 | }
79 |
80 | .container {
81 | background: rgba(255, 255, 255, 0.1);
82 | backdrop-filter: blur(10px);
83 | border-radius: 15px;
84 | padding: 30px;
85 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
86 | text-align: center;
87 | }
88 |
89 | h1 {
90 | font-size: 2.5em;
91 | margin-bottom: 20px;
92 | text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
93 | }
94 |
95 | .input-group {
96 | margin: 20px 0;
97 | }
98 |
99 | input[type="text"] {
100 | padding: 12px 20px;
101 | font-size: 16px;
102 | border: none;
103 | border-radius: 25px;
104 | width: 250px;
105 | text-align: center;
106 | background: rgba(255, 255, 255, 0.9);
107 | color: #333;
108 | }
109 |
110 | button {
111 | background: #ff6b6b;
112 | color: white;
113 | border: none;
114 | padding: 12px 25px;
115 | font-size: 16px;
116 | border-radius: 25px;
117 | cursor: pointer;
118 | margin: 10px;
119 | transition: all 0.3s ease;
120 | }
121 |
122 | button:hover {
123 | background: #ff5252;
124 | transform: translateY(-2px);
125 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
126 | }
127 |
128 | .result {
129 | margin: 20px 0;
130 | padding: 20px;
131 | background: rgba(255, 255, 255, 0.1);
132 | border-radius: 10px;
133 | font-size: 18px;
134 | min-height: 50px;
135 | display: flex;
136 | align-items: center;
137 | justify-content: center;
138 | }
139 |
140 | .loading {
141 | display: none;
142 | color: #ffd700;
143 | }
144 |
145 | .timestamp {
146 | font-size: 14px;
147 | color: rgba(255, 255, 255, 0.8);
148 | margin-top: 20px;
149 | }
150 | </style>
151 | </head>
152 | <body>
153 | <div class="container">
154 | <h1>🌟 Hello World App 🌟</h1>
155 | <p>Welcome to your Google Apps Script web application!</p>
156 |
157 | <div class="input-group">
158 | <input type="text" id="nameInput" placeholder="Enter your name (optional)" />
159 | </div>
160 |
161 | <div>
162 | <button onclick="sayHello()">Say Hello</button>
163 | <button onclick="getTime()">Get Current Time</button>
164 | </div>
165 |
166 | <div id="result" class="result">
167 | Click a button to see the magic! ✨
168 | </div>
169 |
170 | <div id="loading" class="loading">Loading...</div>
171 |
172 | <div id="timestamp" class="timestamp"></div>
173 | </div>
174 |
175 | <script>
176 | function sayHello() {
177 | showLoading();
178 | const name = document.getElementById('nameInput').value;
179 |
180 | google.script.run
181 | .withSuccessHandler(onSuccess)
182 | .withFailureHandler(onFailure)
183 | .getGreeting(name);
184 | }
185 |
186 | function getTime() {
187 | showLoading();
188 |
189 | google.script.run
190 | .withSuccessHandler(onTimeSuccess)
191 | .withFailureHandler(onFailure)
192 | .getCurrentTime();
193 | }
194 |
195 | function onSuccess(result) {
196 | hideLoading();
197 | document.getElementById('result').innerHTML = \`
198 | <div style="font-size: 20px; font-weight: bold;">
199 | \${result}
200 | </div>
201 | \`;
202 | }
203 |
204 | function onTimeSuccess(result) {
205 | hideLoading();
206 | document.getElementById('result').innerHTML = \`
207 | <div style="font-size: 18px;">
208 | 🕐 Current server time: <br>
209 | <strong>\${result}</strong>
210 | </div>
211 | \`;
212 | }
213 |
214 | function onFailure(error) {
215 | hideLoading();
216 | document.getElementById('result').innerHTML = \`
217 | <div style="color: #ff6b6b;">
218 | ❌ Error: \${error.message}
219 | </div>
220 | \`;
221 | }
222 |
223 | function showLoading() {
224 | document.getElementById('loading').style.display = 'block';
225 | document.getElementById('result').style.display = 'none';
226 | }
227 |
228 | function hideLoading() {
229 | document.getElementById('loading').style.display = 'none';
230 | document.getElementById('result').style.display = 'flex';
231 | }
232 |
233 | // Initialize the page
234 | window.onload = function() {
235 | document.getElementById('timestamp').innerHTML =
236 | \`Page loaded at: \${new Date().toLocaleString()}\`;
237 | };
238 | </script>
239 | </body>
240 | </html>`
241 | }
242 | ]
243 | };
244 |
245 | console.log('Updated manifest:', JSON.stringify(updatedManifest, null, 2));
246 |
247 | // Update the script content
248 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/content`, {
249 | method: 'PUT',
250 | headers: {
251 | 'Authorization': `Bearer ${token}`,
252 | 'Content-Type': 'application/json',
253 | 'Accept': 'application/json'
254 | },
255 | body: JSON.stringify(scriptContent)
256 | });
257 |
258 | if (!response.ok) {
259 | const errorText = await response.text();
260 | console.error('Error response:', errorText);
261 | throw new Error(`HTTP ${response.status}: ${errorText}`);
262 | }
263 |
264 | const data = await response.json();
265 | console.log('✅ Script updated successfully');
266 |
267 | return data;
268 | } catch (error) {
269 | console.error('Error updating script:', error);
270 | return null;
271 | }
272 | }
273 |
274 | async function createNewVersion() {
275 | try {
276 | const token = await getOAuthAccessToken();
277 |
278 | console.log('Creating new version...');
279 |
280 | const versionData = {
281 | description: "Version with web app configuration"
282 | };
283 |
284 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/versions`, {
285 | method: 'POST',
286 | headers: {
287 | 'Authorization': `Bearer ${token}`,
288 | 'Content-Type': 'application/json',
289 | 'Accept': 'application/json'
290 | },
291 | body: JSON.stringify(versionData)
292 | });
293 |
294 | if (!response.ok) {
295 | const errorText = await response.text();
296 | console.error('Error response:', errorText);
297 | throw new Error(`HTTP ${response.status}: ${errorText}`);
298 | }
299 |
300 | const data = await response.json();
301 | console.log('✅ New version created:', data.versionNumber);
302 |
303 | return data;
304 | } catch (error) {
305 | console.error('Error creating version:', error);
306 | return null;
307 | }
308 | }
309 |
310 | async function createWebAppDeployment(versionNumber) {
311 | try {
312 | const token = await getOAuthAccessToken();
313 |
314 | console.log('Creating web app deployment...');
315 |
316 | const deploymentConfig = {
317 | description: "Hello World Web App - Public Access",
318 | manifestFileName: "appsscript",
319 | versionNumber: versionNumber
320 | };
321 |
322 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/deployments`, {
323 | method: 'POST',
324 | headers: {
325 | 'Authorization': `Bearer ${token}`,
326 | 'Content-Type': 'application/json',
327 | 'Accept': 'application/json'
328 | },
329 | body: JSON.stringify(deploymentConfig)
330 | });
331 |
332 | if (!response.ok) {
333 | const errorText = await response.text();
334 | console.error('Error response:', errorText);
335 | throw new Error(`HTTP ${response.status}: ${errorText}`);
336 | }
337 |
338 | const data = await response.json();
339 | console.log('✅ Deployment created:', JSON.stringify(data, null, 2));
340 |
341 | return data;
342 | } catch (error) {
343 | console.error('Error creating deployment:', error);
344 | return null;
345 | }
346 | }
347 |
348 | async function getDeploymentDetails(deploymentId) {
349 | try {
350 | const token = await getOAuthAccessToken();
351 | console.log(`Getting deployment details for: ${deploymentId}`);
352 |
353 | const response = await fetch(`https://script.googleapis.com/v1/projects/${scriptId}/deployments/${deploymentId}`, {
354 | method: 'GET',
355 | headers: {
356 | 'Authorization': `Bearer ${token}`,
357 | 'Accept': 'application/json'
358 | }
359 | });
360 |
361 | if (!response.ok) {
362 | const errorText = await response.text();
363 | console.error('Error response:', errorText); throw new Error(`HTTP ${response.status}: ${errorText}`);
364 | }
365 |
366 | const data = await response.json();
367 | console.log('Deployment details:', JSON.stringify(data, null, 2));
368 |
369 | if (data.entryPoints && data.entryPoints[0] && data.entryPoints[0].webApp) {
370 | console.log('\n🎉 SUCCESS! Your web app is deployed!');
371 | console.log('Web App URL:', data.entryPoints[0].webApp.url);
372 | console.log('Access Level:', data.entryPoints[0].webApp.access);
373 | console.log('Execute As:', data.entryPoints[0].webApp.executeAs);
374 | console.log('\n📱 You can now access your Hello World app at the URL above!');
375 | } else {
376 | console.log('\n⚠️ No web app entry point found.');
377 | }
378 |
379 | return data;
380 | } catch (error) {
381 | console.error('Error getting deployment details:', error);
382 | return null;
383 | }
384 | }
385 |
386 | async function main() {
387 | console.log('=== Creating Google Apps Script Web App ===\n');
388 |
389 | // Step 1: Update script with web app configuration
390 | console.log('Step 1: Updating script with web app configuration...');
391 | const updateResult = await updateScriptWithWebAppConfig();
392 | if (!updateResult) return;
393 |
394 | console.log('\n' + '='.repeat(50) + '\n');
395 |
396 | // Step 2: Create a new version
397 | console.log('Step 2: Creating a new version...');
398 | const versionResult = await createNewVersion();
399 | if (!versionResult) return;
400 |
401 | console.log('\n' + '='.repeat(50) + '\n');
402 |
403 | // Step 3: Create deployment with the new version
404 | console.log('Step 3: Creating deployment...');
405 | const deploymentResult = await createWebAppDeployment(versionResult.versionNumber);
406 | if (!deploymentResult) return;
407 |
408 | console.log('\n' + '='.repeat(50) + '\n');
409 |
410 | // Step 4: Get deployment details to see the web app URL
411 | console.log('Step 4: Getting deployment details...');
412 | await getDeploymentDetails(deploymentResult.deploymentId);
413 | }
414 |
415 | main().catch(console.error);
416 |
```