Abstract
This text introduces the Model Context Protocol (MCP) for standardizing AI interaction with external systems. It explores the potential of using Google Apps Script (GAS) to host an MCP server, leveraging GAS’s integration with Google Workspace for data access. A sample script demonstrates feasibility, highlighting the current absence of an official GAS SDK. The work aims to foster understanding and encourage SDK development.
Introduction
Recently, the Model Context Protocol (MCP) has emerged as a standard protocol for connecting AI applications with third-party systems and data sources. Acting like a universal adapter or “USB-C for AI,” the MCP standardizes how AI models can dynamically discover and interact with external resources, tools, and context, often incorporating mechanisms for user consent and secure communication. The detailed specification of this protocol can be confirmed at the official site. Ref
Upon reviewing the specification, it became apparent that an MCP server might be feasible to launch using Google Apps Script. Google Apps Script offers seamless integration with Google Workspace resources such as Google Docs, Google Sheets, Google Slides, Google Calendars, Google Forms, Gmail, and more. The potential to launch an MCP server using Google Apps Script is compelling because it could serve as a powerful gateway, enabling AI applications to securely access and leverage data and functionality within these widely used Google services via a standardized protocol. This opens up possibilities for building AI-powered workflows and applications deeply integrated with personal or organizational Google data.
To explore this potential, I created a sample script demonstrating the feasibility of implementing an MCP server within the Google Apps Script environment. While official SDKs for MCP in languages like TypeScript, Python, Java, Kotlin, and C# have been released to simplify client and server development Ref, an official SDK specifically for Google Apps Script has not yet been released. It is important to note that Google Apps Script has certain limitations regarding execution time and quotas, which would need to be considered for a production-ready server, making it potentially more suitable for specific internal or user-centric applications rather than high-volume public services. Therefore, I hope this report and the accompanying sample script will be useful for understanding the MCP specification in the context of Google Apps Script and will encourage the development of a dedicated SDK for this platform, further facilitating the integration of AI with Google Workspace.
Usage
1. Create a Google Apps Script project
Please create a Google Apps Script project of the standalone type. Ref Of course, the sample script in this report can also be used for the container-bound script type.
Open the script editor of the created Google Apps Script project.
2. Install a library
Repository
https://github.com/tanaikech/MCPApp
Library’s project key
1TlX_L9COAriBlAYvrMLiRFQ5WVf1n0jChB6zHamq2TNwuSbVlI5sBUzh
In order to simply deploy the MCP server, I created the script as a Google Apps Script library. To use this library, please install the library as follows.
- Create a GAS project: You can use this library for the GAS project of both the standalone type and the container-bound script type.
- Install this library: The library’s project key is
1TlX_L9COAriBlAYvrMLiRFQ5WVf1n0jChB6zHamq2TNwuSbVlI5sBUzh
.
3. Script
You can see the whole script, including the library, at my repository. https://github.com/tanaikech/MCPApp
The sample script is as follows: Please copy and paste the following script into the script editor and save the script. MCPApp
is an identifier of the installed library.
In this sample script, “Tools”, “Resources”, and “Prompts” are implemented. At “Tools” and “Resources”, the response value is created from Google Drive and Google Calendar using the functions and returned to the MCP client.
/**
* This function is automatically run when the MCP client accesses Web Apps.
*/
function doPost(eventObject) {
const object = {
eventObject,
serverResponse: getserverResponse_(),
functions: getFunctions_(),
};
return new MCPApp.mcpApp({ accessKey: "sample" }).server(object);
}
The function getserverResponse_()
is as follows. Please check the comment.
/**
* Please set and modify the following JSON to your situation.
* The key is the method from the MCP client.
* The value is the object for returning to the MCP client.
* ID is automatically set in the script.
* The specification of this can be seen in the official document.
* Ref: https://modelcontextprotocol.io/specification/2025-03-26
*/
function getserverResponse_() {
return {
/**
* Response to "initialize"
*/
initialize: {
jsonrpc: "2.0",
result: {
protocolVersion: "2024-11-05", // or "2025-03-26"
capabilities: {
experimental: {},
prompts: {
listChanged: false,
},
resources: {
subscribe: false,
listChanged: false,
},
tools: {
listChanged: false,
},
},
serverInfo: {
name: "sample server from MCPApp",
version: "1.0.0",
},
},
},
/**
* Response to "tools/list"
*/
"tools/list": {
jsonrpc: "2.0",
result: {
tools: [
{
name: "search_files_on_Google_Drive", // <--- It is required to create a function of the same name as this.
description: "Search files on Google Drive.",
inputSchema: {
type: "object",
properties: {
folderName: {
description:
"Search files in the folder of this folder name.",
type: "string",
},
},
required: ["folderName"],
},
},
{
name: "search_schedule_on_Google_Calendar", // <--- It is required to create a function of the same name as this.
description: "Search the schedule on Google Calendar.",
inputSchema: {
type: "object",
properties: {
date: {
description:
"Search the schedule on Google Calendar by giving the date.",
type: "string",
format: "date",
},
},
required: ["date"],
},
},
],
},
},
/**
* Response to "resources/list"
*/
"resources/list": {
jsonrpc: "2.0",
result: {
resources: [
{
uri: "get_today_schedule", // <--- It is required to create a function of the same name as this.
name: "today_schedule",
description: "Today's schedule for Tanaike.",
mimeType: "text/plain",
},
],
nextCursor: "next-page-cursor",
},
},
/**
* Response to "prompts/list"
*/
"prompts/list": {
jsonrpc: "2.0",
result: {
prompts: [
{
name: "search_files_from_Google_Drive",
description:
"Search files in the specific folder on Google Drive using tools.",
arguments: [
{
name: "search_files",
description: "Search files.",
required: true,
},
],
},
],
nextCursor: "next-page-cursor",
},
},
/**
* Response to "prompts/get"
*/
"prompts/get": {
jsonrpc: "2.0",
result: {
description: "Search files in the specific folder on Google Drive.",
messages: [
{
role: "user",
content: {
type: "text",
text: "Return file information from a folder of 'sample' on Google Drive.",
},
},
],
},
},
};
}
The function getFunctions_()
is as follows. Please check the comment. These functions are run when the MCP client calls tools/call
and resources/read
.
/**
* "tools/call": The function name is required to be the same as the name declared at "tools/list".
* "resources/read": The function name is required to be the same as the uri declared at "resources/list".
*/
function getFunctions_() {
return {
"tools/call": {
search_files_on_Google_Drive,
search_schedule_on_Google_Calendar,
},
"resources/read": { get_today_schedule },
};
}
Here, search_files_on_Google_Drive
, search_schedule_on_Google_Calendar
, and get_today_schedule
are the functions. The function name is required to be the same as the name declared at “tools/list” and the uri declared at “resources/list”. Also, the format of the return value is required to follow the specification of MCP. This is the specification of this script.
The functions are as follows.
/**
* This function retrieves the file metadata from the specific folder on Google Drive.
*
* This function is run by "tools/call".
* "tools/call": The function name is required to be the same as the name declared at "tools/list".
*/
function search_files_on_Google_Drive(args) {
const { folderName } = args;
try {
const res = [];
const folders = DriveApp.getFoldersByName(folderName);
while (folders.hasNext()) {
const folder = folders.next();
const files = folder.getFiles();
while (files.hasNext()) {
const file = files.next();
res.push({ filename: file.getName(), mimeType: file.getMimeType() });
}
}
const text = res
.map(
({ filename, mimeType }) =>
`Filename is ${filename}. MimeType is ${mimeType}.`
)
.join("\n");
return {
jsonrpc: "2.0",
result: { content: [{ type: "text", text }], isError: false },
};
} catch (err) {
return {
jsonrpc: "2.0",
result: { content: [{ type: "text", text: err.message }], isError: true },
};
}
}
/**
* This function retrieves events from the specific date on Google Calendar.
*
* This function is run by "tools/call".
* "tools/call": The function name is required to be the same as the name declared at "tools/list".
*/
function search_schedule_on_Google_Calendar(args) {
const { date } = args;
try {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(start.getDate() + 1);
const events = CalendarApp.getDefaultCalendar().getEvents(start, end); // or CalendarApp.getCalendarById("###").getEvents(start, end);
const timeZone = Session.getScriptTimeZone();
const text = events
.map(
(e) =>
`${Utilities.formatDate(
e.getStartTime(),
timeZone,
"HH:mm"
)}-${Utilities.formatDate(
e.getEndTime(),
timeZone,
"HH:mm"
)}: ${e.getTitle()}`
)
.join("\n");
return {
jsonrpc: "2.0",
result: { content: [{ type: "text", text }], isError: false },
};
} catch (err) {
return {
jsonrpc: "2.0",
result: { content: [{ type: "text", text: err.message }], isError: true },
};
}
}
/**
* This function retrieves today's events on Google Calendar.
*
* This function is run by "resources/read".
* "resources/read": The function name is required to be the same as the uri declared at "resources/list".
*/
function get_today_schedule() {
const start = new Date();
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(start.getDate() + 1);
const events = CalendarApp.getDefaultCalendar().getEvents(start, end); // or CalendarApp.getCalendarById("###").getEvents(start, end);
const timeZone = Session.getScriptTimeZone();
const contents = events.map((e) => ({
uri: e.getTitle(),
mimeType: "text/plain",
text: `${Utilities.formatDate(
e.getStartTime(),
timeZone,
"HH:mm"
)}-${Utilities.formatDate(
e.getEndTime(),
timeZone,
"HH:mm"
)}: ${e.getTitle()}`,
}));
return { jsonrpc: "2.0", result: { contents } };
}
This is a simple sample. This script can be tested. But, if you want to create your MCP server, please modify and create the value of serverResponse
, and the functions to your situation.
Another approach
Of course, you can use this library by directly copying and pasting it into your script editor. In that case, please copy and paste the script of this library. And modify as follows.
return new MCPApp.mcpApp({ accessKey: "sample" }).server(object);
to
return new MCPApp({ accessKey: "sample" }).server(object);
4. Web Apps
To allow access from the MCP client, the project uses Web Apps built with Google Apps Script. Ref The MCP client can access the MCP server using an HTTP POST request. Thus, the Web Apps can be used as the MCP server.
Detailed information can be found in the official documentation.
Please follow these steps to deploy the Web App in the script editor.
- In the script editor, at the top right, click “Deploy” -> “New deployment”.
- Click “Select type” -> “Web App”.
- Enter the information about the Web App in the fields under “Deployment configuration”.
- Select “Me” for “Execute as”.
- Select “Anyone” for “Who has access to the app:”. In this sample, a simple approach allows requests without an access token. However, a custom API key is used for accessing the Web App.
- Click “Deploy”.
- On the script editor, at the top right, click “Deploy” -> “Test deployments”.
- Copy the Web App URL. It will be similar to
https://script.google.com/macros/s/###/exec
.
It is important to note that when you modify the Google Apps Script for the Web App, you must modify the deployment as a new version. This ensures the modified script is reflected in the Web App. Please be careful about this. Also, you can find more details on this in my report “Redeploying Web Apps without Changing URL of Web Apps for new IDE”.
5. Prepare for testing: Claude Desktop
To test this Web App, Claude Desktop is used. Ref In this case, the claude_desktop_config.json
is configured as follows. Please replace https://script.google.com/macros/s/###/exec
with your Web App URL.
In this sample, a value of sample
is used as an access key for accessing the Web App. In the current stage, it seems that to use the MCP server through the HTTP request with Claude Desktop, mcp-remote
is required to be used.
{
"mcpServers": {
"gas_web_apps": {
"command": "npx",
"args": [
"mcp-remote",
"https://script.google.com/macros/s/###/exec?accessKey=sample"
],
"env": {}
}
}
}
I could also test the MCP server with the Web Apps using Copilot for Visual Studio Code. In that case, the following setting is used.
"mcp": {
"inputs": [],
"servers": {
"gas_web_apps": {
"command": "npx",
"args": [
"mcp-remote",
"https://script.google.com/macros/s/###/exec?accessKey=sample"
],
"env": {}
}
}
}
6. Testing
When Claude Desktop is run, you can see that gas_web_apps
of the MCP server is installed as follows.
The function search_files_on_Google_Drive
is called by the tools/call
method from the MCP client.
The function search_schedule_on_Google_Calendar
is called by the tools/call
method from the MCP client.
The function search_files_from_Google_Drive
is called by the prompts/get
method from the MCP client.
The function get_today_schedule
is called by the resources/read
method from the MCP client. In this sample, the 3 events above are loaded.
Note
- About
protocolVersion
, when it is different between the MCP client and the MCP server, an error occurs. For example, in the current stage, Claude Desktop v0.9.3 usesprotocolVersion
of2024-11-05
. Under this condition, when the MCP server returnsprotocolVersion
of2025-03-26
to the initialize method, no response is returned from the client. - About
result.content
for returning the response from a function, the type of response value depends on the client side. For example, in the case of Claude Desktop, when the type ofarray
is used, an error likeUnsupported content type: array
. This might be resolved in the future update.