@coretext-ai/qa-google-contacts-a58500d5-8331-4ce9-a140-d204a9fae815
Version:
MCP server with google-contacts integration
615 lines • 23.7 kB
JavaScript
import { GoogleOAuthClient } from '../oauth/google-oauth-client.js';
export class GoogleContactsClient {
constructor() {
this.baseUrl = 'https://people.googleapis.com';
// Generate unique session ID for this client instance
this.sessionId = `google-contacts-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
this.logDebug('INIT', 'OAuth client instance created', {
baseUrl: this.baseUrl,
isOAuth: true
});
this.oauthClient = new GoogleOAuthClient();
}
/**
* Get the session ID for this client instance
*/
getSessionId() {
return this.sessionId;
}
/**
* Enhanced debug logging with session context
*/
logDebug(action, message, metadata) {
const timestamp = new Date().toISOString();
const user = process.env.CORETEXT_USER || 'unknown';
const logEntry = {
timestamp,
sessionId: this.sessionId,
user,
integration: 'google-contacts',
component: 'oauth-client',
action,
message,
...(metadata && { metadata })
};
// Use stderr to avoid MCP protocol interference
console.error(`[GOOGLE_CONTACTS-OAUTH-CLIENT] ${JSON.stringify(logEntry)}`);
}
/**
* Initialize the API client
*/
async initialize() {
this.logDebug('INITIALIZE', 'Starting OAuth client initialization', {
hasOAuthClient: true
});
await this.oauthClient.initialize();
this.logDebug('INITIALIZE', 'Google OAuth client initialization completed');
}
/**
* Make authenticated API request
*/
async makeRequest(config) {
const startTime = Date.now();
this.logDebug('REQUEST_START', 'Making authenticated API request', {
method: config.method,
path: config.path,
hasBody: !!config.body,
hasQueryParams: !!config.queryParams
});
const accessToken = await this.oauthClient.getValidAccessToken();
this.logDebug('AUTH_TOKEN', 'Retrieved Google OAuth access token', {
tokenPreview: accessToken ? accessToken.substring(0, 8) + '...' : 'none'
});
const headers = {
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
...config.headers
};
const url = this.buildUrl(config.path, config.pathParams, config.queryParams);
const response = await fetch(url, {
method: config.method,
headers,
body: config.body ? JSON.stringify(config.body) : undefined
});
if (!response.ok) {
if (response.status === 401) {
// Token might be invalid, try to refresh and retry once
await this.oauthClient.getValidAccessToken(); // This will refresh if needed
return this.makeRequest(config); // Retry once
}
const errorText = await response.text();
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`);
}
return response.json();
}
/**
* Create a new contact with specified fields
*/
async createContact(args = {}) {
const pathParams = {};
const queryParams = {
personFields: args.personFields,
...(args.sources !== undefined && { sources: args.sources }),
};
const bodyParams = {
...(args.names !== undefined && { names: args.names }),
...(args.emailAddresses !== undefined && { emailAddresses: args.emailAddresses }),
...(args.phoneNumbers !== undefined && { phoneNumbers: args.phoneNumbers }),
...(args.addresses !== undefined && { addresses: args.addresses }),
...(args.organizations !== undefined && { organizations: args.organizations }),
...(args.biographies !== undefined && { biographies: args.biographies }),
...(args.birthdays !== undefined && { birthdays: args.birthdays }),
...(args.urls !== undefined && { urls: args.urls }),
};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'POST',
path: '/v1/people:createContact',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Get a specific contact by resource name
*/
async getPerson(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {
personFields: args.personFields,
...(args.sources !== undefined && { sources: args.sources }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/{resourceName}',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Update an existing contact
*/
async updateContact(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {
updatePersonFields: args.updatePersonFields,
...(args.personFields !== undefined && { personFields: args.personFields }),
...(args.sources !== undefined && { sources: args.sources }),
};
const bodyParams = {
...(args.names !== undefined && { names: args.names }),
...(args.emailAddresses !== undefined && { emailAddresses: args.emailAddresses }),
...(args.phoneNumbers !== undefined && { phoneNumbers: args.phoneNumbers }),
...(args.addresses !== undefined && { addresses: args.addresses }),
...(args.organizations !== undefined && { organizations: args.organizations }),
...(args.biographies !== undefined && { biographies: args.biographies }),
...(args.birthdays !== undefined && { birthdays: args.birthdays }),
...(args.urls !== undefined && { urls: args.urls }),
...(args.etag !== undefined && { etag: args.etag }),
};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'PATCH',
path: '/v1/{resourceName}:updateContact',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Delete a contact permanently
*/
async deleteContact(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'DELETE',
path: '/v1/{resourceName}:deleteContact',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* List authenticated user's contacts
*/
async listConnections(args = {}) {
const pathParams = {};
const queryParams = {
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.pageToken !== undefined && { pageToken: args.pageToken }),
personFields: args.personFields,
...(args.sortOrder !== undefined && { sortOrder: args.sortOrder }),
...(args.sources !== undefined && { sources: args.sources }),
...(args.syncToken !== undefined && { syncToken: args.syncToken }),
...(args.requestSyncToken !== undefined && { requestSyncToken: args.requestSyncToken }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/people/me/connections',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Search across all contacts with text query
*/
async searchContacts(args = {}) {
const pathParams = {};
const queryParams = {
query: args.query,
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.readMask !== undefined && { readMask: args.readMask }),
...(args.sources !== undefined && { sources: args.sources }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/people:searchContacts',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Get multiple contacts by resource names
*/
async batchGetPeople(args = {}) {
const pathParams = {};
const queryParams = {
resourceNames: args.resourceNames,
personFields: args.personFields,
...(args.sources !== undefined && { sources: args.sources }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/people:batchGet',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* List all contact groups
*/
async listContactGroups(args = {}) {
const pathParams = {};
const queryParams = {
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.pageToken !== undefined && { pageToken: args.pageToken }),
...(args.groupFields !== undefined && { groupFields: args.groupFields }),
...(args.syncToken !== undefined && { syncToken: args.syncToken }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/contactGroups',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Create a new contact group
*/
async createContactGroup(args = {}) {
const pathParams = {};
const queryParams = {
...(args.readGroupFields !== undefined && { readGroupFields: args.readGroupFields }),
};
const bodyParams = {
contactGroup: args.contactGroup,
};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'POST',
path: '/v1/contactGroups',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Get a specific contact group by resource name
*/
async getContactGroup(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {
...(args.maxMembers !== undefined && { maxMembers: args.maxMembers }),
...(args.groupFields !== undefined && { groupFields: args.groupFields }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/{resourceName}',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Update an existing contact group
*/
async updateContactGroup(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {
...(args.readGroupFields !== undefined && { readGroupFields: args.readGroupFields }),
};
const bodyParams = {
contactGroup: args.contactGroup,
};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'PUT',
path: '/v1/{resourceName}',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Delete a contact group
*/
async deleteContactGroup(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {
...(args.deleteContacts !== undefined && { deleteContacts: args.deleteContacts }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'DELETE',
path: '/v1/{resourceName}',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Add or remove members from a contact group
*/
async modifyContactGroupMembers(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {};
const bodyParams = {
...(args.resourceNamesToAdd !== undefined && { resourceNamesToAdd: args.resourceNamesToAdd }),
...(args.resourceNamesToRemove !== undefined && { resourceNamesToRemove: args.resourceNamesToRemove }),
};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'POST',
path: '/v1/{resourceName}/members:modify',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* List people in the authenticated user's domain directory (G Suite/Workspace)
*/
async listDirectoryPeople(args = {}) {
const pathParams = {};
const queryParams = {
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.pageToken !== undefined && { pageToken: args.pageToken }),
readMask: args.readMask,
sources: args.sources,
...(args.mergeSources !== undefined && { mergeSources: args.mergeSources }),
...(args.syncToken !== undefined && { syncToken: args.syncToken }),
...(args.requestSyncToken !== undefined && { requestSyncToken: args.requestSyncToken }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/people:listDirectoryPeople',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Search people in the authenticated user's domain directory
*/
async searchDirectoryPeople(args = {}) {
const pathParams = {};
const queryParams = {
query: args.query,
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.pageToken !== undefined && { pageToken: args.pageToken }),
readMask: args.readMask,
sources: args.sources,
...(args.mergeSources !== undefined && { mergeSources: args.mergeSources }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/people:searchDirectoryPeople',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* List other contacts (auto-created contacts from interactions)
*/
async listOtherContacts(args = {}) {
const pathParams = {};
const queryParams = {
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.pageToken !== undefined && { pageToken: args.pageToken }),
readMask: args.readMask,
...(args.sources !== undefined && { sources: args.sources }),
...(args.syncToken !== undefined && { syncToken: args.syncToken }),
...(args.requestSyncToken !== undefined && { requestSyncToken: args.requestSyncToken }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/otherContacts',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Search other contacts with text query
*/
async searchOtherContacts(args = {}) {
const pathParams = {};
const queryParams = {
query: args.query,
...(args.pageSize !== undefined && { pageSize: args.pageSize }),
...(args.readMask !== undefined && { readMask: args.readMask }),
};
const bodyParams = {};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'GET',
path: '/v1/otherContacts:search',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Copy an other contact to the authenticated user's contacts
*/
async copyOtherContact(args = {}) {
const pathParams = {
resourceName: args.resourceName,
};
const queryParams = {};
const bodyParams = {
copyMask: args.copyMask,
...(args.sources !== undefined && { sources: args.sources }),
};
// Remove undefined values from pathParams
Object.keys(pathParams).forEach(key => {
if (pathParams[key] === undefined) {
delete pathParams[key];
}
});
return this.makeRequest({
method: 'POST',
path: '/v1/{resourceName}:copyOtherContactToMyContactsGroup',
pathParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
body: Object.keys(bodyParams).length ? bodyParams : undefined
});
}
/**
* Build URL with path parameters and query string
*/
buildUrl(path, pathParams, queryParams) {
let url = this.baseUrl + path;
// Replace path parameters
if (pathParams) {
for (const [key, value] of Object.entries(pathParams)) {
url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
}
}
// Add query parameters
if (queryParams && Object.keys(queryParams).length > 0) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
return url;
}
/**
* Check if client is authenticated
*/
async isAuthenticated() {
return this.oauthClient.isAuthenticated();
}
/**
* Revoke authentication tokens
*/
async revokeAuthentication() {
await this.oauthClient.revokeTokens();
}
}
//# sourceMappingURL=google-contacts-client.js.map