Skip to content

Google Sheets #1156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7514990
feat: create app
stherzada May 20, 2025
0de7836
chore: lint
stherzada May 20, 2025
c09d213
feat: implement OAuth refresh token functionality
JonasJesus42 May 21, 2025
88b3e08
feat: add token manager and auto refresh utilities
JonasJesus42 May 21, 2025
53ec4b3
feat: implement token validation and auto refresh in data loaders
JonasJesus42 May 21, 2025
da20fee
chore: update constants and interfaces for OAuth refresh flow
JonasJesus42 May 21, 2025
d815c47
refactor: move context to global mcp folder and fix formatting
JonasJesus42 May 21, 2025
d4b843b
fix: flow oauth
JonasJesus42 May 21, 2025
4e70f98
update oauth thingies
viktormarinho May 22, 2025
70a7a7e
format
viktormarinho May 22, 2025
70ba8b5
getSpreadsheet works
viktormarinho May 22, 2025
b62d54d
feat: add clientId and clientSecret to refreshToken
JonasJesus42 May 23, 2025
116c725
feat: create MCP OAuth infrastructure
JonasJesus42 May 23, 2025
1e21d9d
refactor: remove duplicate OAuth utilities
JonasJesus42 May 23, 2025
862301f
refactor: migrate Google Sheets to unified OAuth
JonasJesus42 May 23, 2025
c07801e
fix: TypeScript compliance in LRU cache
JonasJesus42 May 23, 2025
6fac571
remove: unused OAuth HTTP client
JonasJesus42 May 23, 2025
e9c9902
fix: return errors instead of throwing in updateValues
JonasJesus42 May 23, 2025
b4e00be
improve: enhance typing and documentation for AI usage
JonasJesus42 May 23, 2025
f79f194
fix: Google Sheets API typing and validation improvements - Fix value…
JonasJesus42 May 23, 2025
d4b2cdd
fix: remove duplicate range parameter in updateValues API call
JonasJesus42 May 23, 2025
21fc1bb
feat: add simplified types and mapping system for Google Sheets API -…
JonasJesus42 May 23, 2025
aa0d53b
refactor: reorganize type definitions and create utility modules
JonasJesus42 May 23, 2025
5c58d0a
feat: simplify Google Sheets actions with LLM-friendly interfaces
JonasJesus42 May 23, 2025
661205b
feat: add semantic @name annotations to actions
JonasJesus42 May 23, 2025
aa1d2ad
feat: add semantic @name annotations to loaders
JonasJesus42 May 23, 2025
11341cd
style: apply automatic Deno formatting
JonasJesus42 May 23, 2025
4ebb0fa
fix: improve OAuth provider configuration
JonasJesus42 May 23, 2025
3f9a837
fix: make OAuth credentials required in OAuthProvider interface
JonasJesus42 May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions google-sheets/actions/batchUpdateValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { AppContext } from "../mod.ts";
import type {
Result,
SimpleBatchUpdateProps,
SimpleBatchUpdateResponse,
SimpleError,
SimpleValueRange,
} from "../utils/types.ts";
import {
mapApiBatchUpdateResponseToSimple,
mapSimpleBatchUpdatePropsToApi,
parseApiErrorText,
validateSimpleBatchUpdateProps,
} from "../utils/mappers.ts";
import { buildFullRange } from "../utils/rangeUtils.ts";

/**
* Simplified props for batch updating values
*/
export interface Props {
/**
* @title First Cell Location
* @description The starting cell for the update range, specified in A1 notation (e.g., 'A1', 'B2')
*/
first_cell_location?: string;

/**
* @title Include Values in Response
* @description Whether to return the updated values in the API response
*/
includeValuesInResponse?: boolean;

/**
* @title Sheet Name
* @description The name of the specific sheet within the spreadsheet to update
*/
sheet_name: string;

/**
* @title Spreadsheet ID
* @description The unique identifier of the Google Sheets spreadsheet to be updated
*/
spreadsheet_id: string;

/**
* @title Value Input Option
* @description Use 'RAW' to store values as-is or 'USER_ENTERED' to interpret formulas
*/
valueInputOption?: "RAW" | "USER_ENTERED";

/**
* @title Values
* @description A 2D list representing the values to update. Each inner list corresponds to a row in the spreadsheet
*/
values: string[][];
}

/**
* Maps simplified props to the expected API format
*/
function mapPropsToApiFormat(props: Props): SimpleBatchUpdateProps {
const firstCell = props.first_cell_location || "A1";
const range = buildFullRange(props.sheet_name, firstCell, props.values);

const simpleData: SimpleValueRange[] = [{
range: range,
values: props.values,
majorDimension: "ROWS",
}];

return {
spreadsheetId: props.spreadsheet_id,
data: simpleData,
valueInputOption: props.valueInputOption || "USER_ENTERED",
includeValuesInResponse: props.includeValuesInResponse || false,
responseValueRenderOption: "FORMATTED_VALUE",
responseDateTimeRenderOption: "SERIAL_NUMBER",
};
}

/**
* @name BATCH_UPDATE_SPREADSHEET_VALUES
* @title Batch Update Spreadsheet Values
* @description Updates values in a Google Sheets spreadsheet in a simple and intuitive way. Just specify the sheet name, starting cell and data, and the system will automatically calculate the required range.
*/
const action = async (
props: Props,
_req: Request,
ctx: AppContext,
): Promise<Result<SimpleBatchUpdateResponse>> => {
// Basic validations
if (!props.spreadsheet_id || !props.sheet_name || !props.values) {
return {
message:
"Missing required parameters: spreadsheet_id, sheet_name and values are required",
} as SimpleError;
}

if (!Array.isArray(props.values) || props.values.length === 0) {
return {
message: "The 'values' parameter must be a non-empty array of arrays",
} as SimpleError;
}

// Check if all elements of values are arrays
if (!props.values.every((row) => Array.isArray(row))) {
return {
message: "All elements in 'values' must be arrays (representing rows)",
} as SimpleError;
}

try {
// Map simple props to API format
const simpleProps = mapPropsToApiFormat(props);

// Validate mapped props
const validationErrors = validateSimpleBatchUpdateProps(simpleProps);
if (validationErrors.length > 0) {
return {
message: `Validation error: ${validationErrors.join(", ")}`,
} as SimpleError;
}

// Map to Google API format
const { body, params } = mapSimpleBatchUpdatePropsToApi(simpleProps);

// Make API call
const response = await ctx.client
["POST /v4/spreadsheets/:spreadsheetId/values:batchUpdate"](
params,
{ body },
);

if (!response.ok) {
const errorText = await response.text();
return parseApiErrorText(errorText);
}

// Map API response to simple format
const apiResponse = await response.json();
return mapApiBatchUpdateResponseToSimple(apiResponse);
} catch (error) {
return {
message: `Communication error: ${
error instanceof Error ? error.message : "Unknown error"
}`,
details: { originalError: error },
} as SimpleError;
}
};

export default action;
106 changes: 106 additions & 0 deletions google-sheets/actions/createSpreadsheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { AppContext } from "../mod.ts";
import { Spreadsheet } from "../utils/types.ts";

export interface Props {
/**
* @title Spreadsheet Title
* @description The title for the new spreadsheet
*/
title: string;

/**
* @title Locale
* @description The locale code for the spreadsheet
*/
locale?: string;

/**
* @title Time Zone
* @description The time zone of the spreadsheet
*/
timeZone?: string;

/**
* @title Auto Recalculate
* @description When formulas should be recalculated
*/
autoRecalc?: "ON_CHANGE" | "MINUTE" | "HOUR";

/**
* @title Initial Sheets
* @description Configuration of the initial sheets in the spreadsheet
*/
sheets?: Array<{
/**
* @title Sheet Title
* @description Title of the sheet to be created
*/
title: string;

/**
* @title Row Count
* @description Number of rows in the sheet
*/
rowCount?: number;

/**
* @title Column Count
* @description Number of columns in the sheet
*/
columnCount?: number;
}>;
}

/**
* @name CREATE_SPREADSHEET
* @title Create Spreadsheet
* @description Creates a new spreadsheet in Google Sheets
*/
const action = async (
props: Props,
_req: Request,
ctx: AppContext,
): Promise<Spreadsheet | { error: string }> => {
const {
title,
locale = "pt_BR",
timeZone = "America/Sao_Paulo",
autoRecalc = "ON_CHANGE",
sheets = [{ title: "Sheet1", rowCount: 1000, columnCount: 26 }],
} = props;

const { client } = ctx;

const requestBody = {
properties: {
title,
locale,
timeZone,
autoRecalc,
},
sheets: sheets.map((sheet) => ({
properties: {
title: sheet.title,
gridProperties: {
rowCount: sheet.rowCount,
columnCount: sheet.columnCount,
},
},
})),
};

const response = await client["POST /v4/spreadsheets"]({}, {
body: requestBody,
});

if (!response.ok) {
const errorText = await response.text();
return {
error: errorText,
};
}

return await response.json();
};

export default action;
68 changes: 68 additions & 0 deletions google-sheets/actions/oauth/callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { AppContext } from "../../mod.ts";

interface OAuthCallbackResponse {
access_token: string;
expires_in: number;
refresh_token: string;
scope: string;
token_type: string;
}

export interface Props {
code: string;
installId: string;
clientId: string;
clientSecret: string;
redirectUri: string;
}

/**
* @name OAUTH_CALLBACK
* @title OAuth Google Callback
* @description Exchanges the authorization code for access tokens
*/
export default async function callback(
{ code, installId, clientId, clientSecret, redirectUri }: Props,
req: Request,
ctx: AppContext,
): Promise<{ installId: string }> {
const { client } = ctx;

const finalRedirectUri = redirectUri ||
new URL("/oauth/callback", req.url).href;

const response = await client["POST /token"]({
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: finalRedirectUri,
grant_type: "authorization_code",
});

const tokenData = await response.json() as OAuthCallbackResponse;
const currentTime = Math.floor(Date.now() / 1000);

client.oauth.tokens.access_token = tokenData.access_token;
client.oauth.tokens.refresh_token = tokenData.refresh_token;
client.oauth.tokens.expires_in = tokenData.expires_in;
client.oauth.tokens.scope = tokenData.scope;
client.oauth.tokens.token_type = tokenData.token_type;
client.oauth.tokens.tokenObtainedAt = currentTime;

const currentCtx = await ctx.getConfiguration();
await ctx.configure({
...currentCtx,
tokens: {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expires_in: tokenData.expires_in,
scope: tokenData.scope,
token_type: tokenData.token_type,
tokenObtainedAt: currentTime,
},
clientSecret: clientSecret,
clientId: clientId,
});

return { installId };
}
Loading
Loading