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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions server/mcp-core/auth-context-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface AuthContext {
// Multi-provider tokens (nested structure per Q21, Q22)
atlassian?: ProviderAuthInfo;
figma?: ProviderAuthInfo;
google?: ProviderAuthInfo;
// JWT metadata (preserved for compatibility)
iss?: string;
aud?: string;
Expand Down Expand Up @@ -68,6 +69,7 @@ function getTokenLogInfo(token?: string, prefix: string = 'Token'): Record<strin
export function setAuthContext(transportId: string, authInfo: AuthContext): void {
const atlassianToken = authInfo?.atlassian?.access_token;
const figmaToken = authInfo?.figma?.access_token;
const googleToken = authInfo?.google?.access_token;
const now = Math.floor(Date.now() / 1000);

// Calculate expiry for Atlassian tokens
Expand All @@ -92,6 +94,11 @@ export function setAuthContext(transportId: string, authInfo: AuthContext): void
hasRefreshToken: !!authInfo.figma.refresh_token,
scope: authInfo.figma.scope,
} : null,
google: authInfo?.google ? {
...getTokenLogInfo(googleToken, 'googleToken'),
hasRefreshToken: !!authInfo.google.refresh_token,
scope: authInfo.google.scope,
} : null,
},
issuer: authInfo?.iss,
audience: authInfo?.aud,
Expand Down Expand Up @@ -146,7 +153,7 @@ export function getAuthContext(transportId: string): AuthContext | undefined {
*/
export function getAuthInfo(context: any): AuthContext | null {
// First try to get from context if it's directly available
if (context?.authInfo && (context.authInfo.atlassian || context.authInfo.figma)) {
if (context?.authInfo && (context.authInfo.atlassian || context.authInfo.figma || context.authInfo.google)) {
return context.authInfo;
}

Expand All @@ -156,7 +163,7 @@ export function getAuthInfo(context: any): AuthContext | null {
if (sessionId) {
const authInfo = authContextStore.get(sessionId);

if (authInfo && (authInfo.atlassian || authInfo.figma)) {
if (authInfo && (authInfo.atlassian || authInfo.figma || authInfo.google)) {
Copy link
Member

Choose a reason for hiding this comment

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

I think we should do some cleanup to make our auth providers more abstract and code like this just loops through them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would suggest creating a new ticket to improve the overall authentication code. I would rather not add new code to the current implementation on this MR. Do you mind creating a ticket and assigning it to me?

return authInfo;
}
}
Expand Down
5 changes: 4 additions & 1 deletion server/mcp-core/auth-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ export function isTokenExpired(authInfo: AuthContext | null): boolean {
const hasValidFigmaToken = authInfo.figma &&
authInfo.figma.expires_at > now;

const hasValidGoogleToken = authInfo.google &&
authInfo.google.expires_at > now;

// If at least one provider has a valid token, not expired
const hasAnyValidToken = hasValidAtlassianToken || hasValidFigmaToken;
const hasAnyValidToken = hasValidAtlassianToken || hasValidFigmaToken || hasValidGoogleToken;

return !hasAnyValidToken;
}
Expand Down
7 changes: 7 additions & 0 deletions server/mcp-core/server-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { AuthContext } from './auth-context-store.ts';
// Import provider implementations
import { atlassianProvider } from '../providers/atlassian/index.js';
import { figmaProvider } from '../providers/figma/index.js';
import { googleProvider } from '../providers/google/index.js';
import { utilityProvider } from '../providers/utility/index.js';
import { combinedProvider } from '../providers/combined/index.js';

Expand Down Expand Up @@ -79,6 +80,12 @@ export function createMcpServer(authContext: AuthContext): McpServer {
registeredProviders.push('figma');
}

if (authContext.google) {
console.log(' Registering Google Drive tools');
googleProvider.registerTools(mcp, authContext);
registeredProviders.push('google');
}

// Register combined tools only if BOTH providers are available
if (authContext.atlassian && authContext.figma) {
console.log(' Registering combined tools (atlassian + figma)');
Expand Down
6 changes: 3 additions & 3 deletions server/mcp-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@ function validateAndExtractJwt(token: string, req: Request, res: Response, sourc
console.log(`Successfully parsed JWT payload from ${source}:`, JSON.stringify(sanitizeJwtPayload(payload), null, 2));

// Validate that we have at least one provider's credentials (nested structure per Q21)
if (!payload.atlassian && !payload.figma) {
console.log(`JWT payload missing provider credentials (${source}) - expected 'atlassian' or 'figma' nested structure`);
if (!payload.atlassian && !payload.figma && !payload.google) {
console.log(`JWT payload missing provider credentials (${source}) - expected 'atlassian', 'figma', or 'google' nested structure`);
sendMissingAtlassianAccessToken(res, req, source);
return { authInfo: null, errored: true };
}
Expand Down Expand Up @@ -371,7 +371,7 @@ function send401(res: Response, jsonResponse: { error: string }, includeInvalidT
}

function sendMissingAtlassianAccessToken(res: Response, req: Request, where: string = 'bearer header'): Response {
const message = `Authentication token missing Atlassian access token in ${where}.`;
const message = `Authentication token missing provider access token in ${where}.`;
console.log(`❌🔑 ${message}`);
return res
.status(401)
Expand Down
14 changes: 13 additions & 1 deletion server/pkce/refresh-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from './token-helpers.ts';
import { atlassianProvider } from '../providers/atlassian/index.ts';
import { figmaProvider } from '../providers/figma/index.ts';
import { googleProvider } from '../providers/google/index.ts';
import type { OAuthHandler, OAuthRequest, OAuthErrorResponse } from './types.ts';

/**
Expand Down Expand Up @@ -107,10 +108,12 @@ export const refreshToken: OAuthHandler = async (req: Request, res: Response): P
// Extract provider refresh tokens from nested structure
const atlassianRefreshToken = refreshPayload.atlassian?.refresh_token;
const figmaRefreshToken = refreshPayload.figma?.refresh_token;
const googleRefreshToken = refreshPayload.google?.refresh_token;

// Refresh each provider using the provider interface
let newAtlassianTokens: any = null;
let newFigmaTokens: any = null;
let newGoogleTokens: any = null;

try {
// Refresh Atlassian if present
Expand All @@ -127,8 +130,15 @@ export const refreshToken: OAuthHandler = async (req: Request, res: Response): P
});
}

// Refresh Google if present
if (googleRefreshToken) {
newGoogleTokens = await googleProvider.refreshAccessToken!({
refreshToken: googleRefreshToken,
});
}

// Check if we refreshed any provider
if (!newAtlassianTokens && !newFigmaTokens) {
if (!newAtlassianTokens && !newFigmaTokens && !newGoogleTokens) {
console.error(
'🔄 REFRESH TOKEN FLOW - ERROR: No provider refresh tokens found in JWT'
);
Expand Down Expand Up @@ -165,6 +175,7 @@ export const refreshToken: OAuthHandler = async (req: Request, res: Response): P
const multiProviderTokens: MultiProviderTokens = {};
addProviderTokens(multiProviderTokens, 'atlassian', newAtlassianTokens);
addProviderTokens(multiProviderTokens, 'figma', newFigmaTokens);
addProviderTokens(multiProviderTokens, 'google', newGoogleTokens);

// Create new access token with refreshed provider tokens
const newAccessToken = await createMultiProviderAccessToken(
Expand Down Expand Up @@ -204,6 +215,7 @@ export const refreshToken: OAuthHandler = async (req: Request, res: Response): P
providers_refreshed: [
newAtlassianTokens && 'atlassian',
newFigmaTokens && 'figma',
newGoogleTokens && 'google',
].filter(Boolean),
expires_in: expiresIn,
});
Expand Down
5 changes: 3 additions & 2 deletions server/pkce/token-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ProviderTokenData {
export interface MultiProviderTokens {
atlassian?: ProviderTokenData;
figma?: ProviderTokenData;
google?: ProviderTokenData;
}

// Extended interface to handle optional refresh token expiration
Expand All @@ -58,12 +59,12 @@ export interface ExtendedAtlassianTokenResponse extends AtlassianTokenResponse {
* Mutates the target object by adding provider token data with calculated expiration
*
* @param target - The MultiProviderTokens object to mutate
* @param providerKey - The provider key ('atlassian' or 'figma')
* @param providerKey - The provider key ('atlassian', 'figma', or 'google')
* @param tokens - Provider token response containing access_token, refresh_token, expires_in, scope
*/
export function addProviderTokens(
target: MultiProviderTokens,
providerKey: 'atlassian' | 'figma',
providerKey: 'atlassian' | 'figma' | 'google',
tokens: any
): void {
if (tokens) {
Expand Down
15 changes: 14 additions & 1 deletion server/provider-server-oauth/connection-done.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ export async function handleConnectionDone(req: Request, res: Response): Promise
// Build multi-provider tokens structure for JWT creation
const atlassianTokens = providerTokens['atlassian'];
const figmaTokens = providerTokens['figma'];
const googleTokens = providerTokens['google'];

if (!atlassianTokens && !figmaTokens) {
if (!atlassianTokens && !figmaTokens && !googleTokens) {
throw new Error('No provider tokens found - please connect at least one service');
}

Expand Down Expand Up @@ -93,6 +94,18 @@ export async function handleConnectionDone(req: Request, res: Response): Promise
console.log(' Warning: Figma tokens incomplete (missing access or refresh token)');
}

if (googleTokens && googleTokens.access_token && googleTokens.refresh_token) {
console.log(' Adding Google credentials to JWT');
multiProviderTokens.google = {
Copy link
Member

Choose a reason for hiding this comment

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

similar here, this code is the same for each provider

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as before. This can be added to the next ticket for authentication improvement.

access_token: googleTokens.access_token,
refresh_token: googleTokens.refresh_token,
expires_at: googleTokens.expires_at,
scope: googleTokens.scope,
};
} else if (googleTokens) {
console.log(' Warning: Google tokens incomplete (missing access or refresh token)');
}

// Create JWT access token with nested provider structure
const tokenOptions = {
resource: req.session.mcpResource || process.env.VITE_AUTH_SERVER_URL,
Expand Down
11 changes: 10 additions & 1 deletion server/provider-server-oauth/connection-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import crypto from 'crypto';

// All providers that must be connected for auto-redirect to /auth/done
// If only some providers are connected, user can manually click "Done" button
const REQUIRED_PROVIDERS = ['atlassian', 'figma'] as const;
const REQUIRED_PROVIDERS = ['atlassian', 'figma', 'google'] as const;

/**
* Renders the connection hub UI
Expand Down Expand Up @@ -185,6 +185,15 @@ export function renderConnectionHub(req: Request, res: Response): void {
}
</div>

<div class="provider ${connectedProviders.includes('google') ? 'connected' : ''}">
<h2>Google Drive</h2>
<p>Access Google Drive files</p>
${connectedProviders.includes('google')
? '<span class="status">✓ Connected</span>'
: '<button onclick="location.href=\'/auth/connect/google\'">Connect Google Drive</button>'
}
</div>

<div class="done-section">
<button class="done-button" onclick="location.href='/auth/done'" ${connectedProviders.length === 0 ? 'disabled' : ''}>
Done - Create Session
Expand Down
66 changes: 66 additions & 0 deletions server/providers/google/google-api-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Google Drive API Client Factory
*
* Provides API client instances for OAuth authentication.
* Uses native fetch (no additional dependencies).
*
* Authentication Methods:
* - OAuth: Uses Bearer tokens from OAuth 2.0 flow (for user delegation)
*/

import type { DriveAboutResponse } from './types.js';

/**
* Google API client interface
*
* Provides methods for making authenticated requests to Google APIs.
* All methods have the access token pre-configured via closure.
*/
export interface GoogleClient {
/**
* Make an authenticated fetch request to Google API
* @param url - The full URL to fetch
* @param options - Standard fetch options (method, body, etc.)
* @returns Promise resolving to fetch Response
*/
fetch: (url: string, options?: RequestInit) => Promise<Response>;

/**
* Authentication type used by this client
*/
authType: 'oauth';
}

/**
* Create a Google API client using OAuth access token
* @param accessToken - OAuth 2.0 Bearer token
* @returns API client with Drive operations
*
* @example
* ```typescript
* const client = createGoogleClient(token);
*
* // Fetch with auth automatically included
* const response = await client.fetch(
* 'https://www.googleapis.com/drive/v3/about?fields=user',
* { method: 'GET' }
* );
* ```
*/
export function createGoogleClient(accessToken: string): GoogleClient {
return {
authType: 'oauth',

fetch: async (url: string, options: RequestInit = {}) => {
// Token is captured in this closure!
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json',
},
});
},
};
}
31 changes: 31 additions & 0 deletions server/providers/google/google-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Google Drive API interaction helpers
* Reusable functions for Google Drive API calls
*/

import type { GoogleClient } from './google-api-client.js';
import type { DriveAboutResponse } from './types.js';

/**
* Get the authenticated user's Google Drive information
* @param client - Authenticated Google API client
* @returns Promise resolving to Drive user information
* @throws Error if the API request fails
*
* @example
* ```typescript
* const client = createGoogleClient(token);
* const userData = await getGoogleDriveUser(client);
* console.log(userData.user.emailAddress);
* ```
*/
export async function getGoogleDriveUser(client: GoogleClient): Promise<DriveAboutResponse> {
const response = await client.fetch('https://www.googleapis.com/drive/v3/about?fields=user');

if (!response.ok) {
const errorText = await response.text();
throw new Error(`Drive API error (${response.status}): ${errorText}`);
}

return response.json() as Promise<DriveAboutResponse>;
}
Loading
Loading