Skip to main content
Glama
unifiedServer.ts25.3 kB
/** * @fileoverview Unified Activity Reporting Server * @description MCP server that supports both GDE (Advocu) and Microsoft MVP activity submissions */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; // GDE imports import type { ContentCreationDraft, GooglerInteractionDraft, MentoringDraft, ProductFeedbackDraft, PublicSpeakingDraft, StoryDraft, WorkshopDraft, } from "./interfaces/index.js"; import { ContentType, Country, EventFormat, InteractionFormat, InteractionType, ProductFeedbackContentType, SignificanceType, } from "./types/index.js"; // MVP imports import type { MVPVideoActivity, MVPBlogActivity, MVPSpeakingActivity } from "./interfaces/mvp/index.js"; import { MVPActivityType, MVPActivityRole, MVPTargetAudience } from "./types/mvp/index.js"; // Get the docs directory from environment variable or fallback // IMPORTANT: Set DOCS_DIR in Claude Desktop config to the full path // Example: /Users/username/work/mcp-mvp-gde/docs const DOCS_DIR = process.env.DOCS_DIR || join(process.cwd(), "docs"); // Define available documentation resources const DOCUMENTATION_RESOURCES = [ { uri: "docs://api-reference", name: "API Reference", description: "Complete API documentation for both Microsoft MVP and Google GDE (Advocu) APIs", mimeType: "text/markdown", file: "API.md", }, { uri: "docs://mvp-api-reference", name: "MVP API Reference", description: "Detailed Microsoft MVP API reference with field specifications and examples", mimeType: "text/markdown", file: "MVP_API_REFERENCE.md", }, { uri: "docs://mvp-fixes-changelog", name: "MVP Integration Fixes Changelog", description: "Complete changelog of fixes applied to MVP integration", mimeType: "text/markdown", file: "CHANGELOG_MVP_FIXES.md", }, { uri: "docs://error-handling", name: "Error Handling Improvements", description: "Documentation of error handling improvements and best practices", mimeType: "text/markdown", file: "ERROR_HANDLING_IMPROVEMENTS.md", }, { uri: "docs://mcp-resources", name: "MCP Resources Guide", description: "Guide on how to use MCP resources to access documentation", mimeType: "text/markdown", file: "MCP_RESOURCES.md", }, ] as const; export class UnifiedActivityReportingServer { private readonly server: Server; // GDE configuration private readonly gdeAccessToken: string | null; private readonly gdeBaseUrl: string; private readonly gdeEnabled: boolean; // MVP configuration private readonly mvpAccessToken: string | null; private readonly mvpUserProfileId: number | null; private readonly mvpBaseUrl: string; private readonly mvpEnabled: boolean; /** * Extract a meaningful error message from various error types */ private getErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } if (typeof error === "string") { return error; } if (error && typeof error === "object") { if ("message" in error && typeof error.message === "string") { return error.message; } if ("error" in error && typeof error.error === "string") { return error.error; } if ("statusText" in error && typeof error.statusText === "string") { return error.statusText; } } return JSON.stringify(error); } constructor() { this.server = new Server( { name: "activity-reporting-server", version: "0.2.0", }, { capabilities: { tools: {}, resources: {}, }, }, ); // Setup GDE this.gdeAccessToken = process.env.ADVOCU_ACCESS_TOKEN || null; this.gdeBaseUrl = process.env.ADVOCU_API_URL ?? "https://api.advocu.com/personal-api/v1/gde"; this.gdeEnabled = !!this.gdeAccessToken; if (!this.gdeEnabled) { console.error("⚠️ ADVOCU_ACCESS_TOKEN not set - GDE tools will be disabled"); } // Setup MVP this.mvpAccessToken = process.env.MVP_ACCESS_TOKEN || null; const mvpProfileId = process.env.MVP_USER_PROFILE_ID || null; this.mvpUserProfileId = mvpProfileId ? Number.parseInt(mvpProfileId, 10) : null; this.mvpBaseUrl = process.env.MVP_API_URL ?? "https://mavenapi-prod.azurewebsites.net/api"; this.mvpEnabled = !!(this.mvpAccessToken && this.mvpUserProfileId); if (!this.mvpEnabled) { if (!this.mvpAccessToken) { console.error("⚠️ MVP_ACCESS_TOKEN not set - MVP tools will be disabled"); } if (!this.mvpUserProfileId) { console.error("⚠️ MVP_USER_PROFILE_ID not set - MVP tools will be disabled"); } } if (!this.gdeEnabled && !this.mvpEnabled) { console.error("❌ Neither GDE nor MVP credentials configured. At least one must be set."); process.exit(1); } console.error(`✅ Server initialized:`); console.error(` GDE: ${this.gdeEnabled ? "enabled" : "disabled"}`); console.error(` MVP: ${this.mvpEnabled ? "enabled" : "disabled"}`); console.error(` Docs: ${DOCS_DIR}`); this.setupToolHandlers(); this.setupResourceHandlers(); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Array<{ name: string; description: string; inputSchema: Record<string, unknown>; }> = []; // Add GDE tools if enabled if (this.gdeEnabled) { tools.push( { name: "submit_gde_content_creation", description: "Submit a content creation activity to Google GDE program (Advocu)", inputSchema: { type: "object", properties: { title: { type: "string", minLength: 3, maxLength: 200 }, description: { type: "string", maxLength: 2000 }, activityDate: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }, contentType: { type: "string", enum: Object.values(ContentType) }, tags: { type: "array", items: { type: "string" }, minItems: 0 }, metrics: { type: "object", properties: { readers: { type: "integer", minimum: 1 } }, required: ["readers"], }, activityUrl: { type: "string", maxLength: 500, pattern: "^https?://.*" }, additionalInfo: { type: "string", maxLength: 2000 }, private: { type: "boolean" }, }, required: ["title", "description", "activityDate", "contentType", "metrics", "activityUrl"], }, }, // Add other GDE tools... ); } // Add documentation tools (always available) tools.push( { name: "list_documentation", description: "List all available documentation resources", inputSchema: { type: "object", properties: {}, }, }, { name: "get_documentation", description: "Get a specific documentation file by name", inputSchema: { type: "object", properties: { documentName: { type: "string", enum: [ "api-reference", "mvp-api-reference", "mvp-fixes-changelog", "error-handling", "mcp-resources", ], description: "Name of the documentation to retrieve", }, }, required: ["documentName"], }, }, ); // Add MVP tools if enabled if (this.mvpEnabled) { tools.push( { name: "submit_mvp_video", description: "Submit a video activity to Microsoft MVP. Valid roles for videos: Host, Presenter, Speaker.", inputSchema: { type: "object", properties: { title: { type: "string", minLength: 1, maxLength: 100 }, description: { type: "string", maxLength: 1000 }, date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }, url: { type: "string", pattern: "^https?://.*" }, targetAudience: { type: "array", items: { type: "string", enum: Object.values(MVPTargetAudience) }, minItems: 1, }, role: { type: "string", enum: ["Host", "Presenter", "Speaker"], description: "Valid roles for video activities" }, technologyFocusArea: { type: "string", description: "Main technology area (e.g., 'Web Development', 'Cloud & AI')" }, liveStreamViews: { type: "integer", minimum: 0 }, onDemandViews: { type: "integer", minimum: 0 }, numberOfSessions: { type: "integer", minimum: 1, default: 1 }, isPrivate: { type: "boolean", default: false }, }, required: [ "title", "description", "date", "url", "targetAudience", "role", "technologyFocusArea", "liveStreamViews", "onDemandViews", ], }, }, { name: "submit_mvp_blog", description: "Submit a blog post activity to Microsoft MVP", inputSchema: { type: "object", properties: { title: { type: "string", minLength: 1, maxLength: 100 }, description: { type: "string", maxLength: 1000 }, date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }, url: { type: "string", pattern: "^https?://.*" }, targetAudience: { type: "array", items: { type: "string", enum: Object.values(MVPTargetAudience) }, minItems: 1, }, role: { type: "string", enum: Object.values(MVPActivityRole) }, technologyFocusArea: { type: "string" }, additionalTechnologyAreas: { type: "array", items: { type: "string" } }, numberOfViews: { type: "integer", minimum: 0 }, subscriberBase: { type: "integer", minimum: 0 }, isPrivate: { type: "boolean", default: false }, }, required: [ "title", "description", "date", "url", "targetAudience", "role", "technologyFocusArea", "numberOfViews", ], }, }, { name: "submit_mvp_speaking", description: "Submit a speaking/conference activity to Microsoft MVP", inputSchema: { type: "object", properties: { title: { type: "string", minLength: 1, maxLength: 100 }, description: { type: "string", maxLength: 1000 }, date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" }, url: { type: "string", pattern: "^https?://.*" }, targetAudience: { type: "array", items: { type: "string", enum: Object.values(MVPTargetAudience) }, minItems: 1, }, role: { type: "string", enum: Object.values(MVPActivityRole) }, technologyFocusArea: { type: "string" }, additionalTechnologyAreas: { type: "array", items: { type: "string" } }, inPersonAttendees: { type: "integer", minimum: 0 }, numberOfSessions: { type: "integer", minimum: 1, default: 1 }, liveStreamViews: { type: "integer", minimum: 0 }, onDemandViews: { type: "integer", minimum: 0 }, isPrivate: { type: "boolean", default: false }, }, required: [ "title", "description", "date", "url", "targetAudience", "role", "technologyFocusArea", "inPersonAttendees", "numberOfSessions", ], }, }, ); } return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Documentation tools if (name === "list_documentation") { return await this.listDocumentation(); } if (name === "get_documentation") { return await this.getDocumentation(args as { documentName: string }); } // MVP tools if (name.startsWith("submit_mvp_")) { if (!this.mvpEnabled) { throw new McpError(ErrorCode.InvalidRequest, "MVP tools are not enabled. Please configure MVP credentials."); } switch (name) { case "submit_mvp_video": return await this.submitMVPVideoActivity(args as unknown as MVPVideoActivity); case "submit_mvp_blog": return await this.submitMVPBlogActivity(args as unknown as MVPBlogActivity); case "submit_mvp_speaking": return await this.submitMVPSpeakingActivity(args as unknown as MVPSpeakingActivity); } } // GDE tools if (name.startsWith("submit_gde_")) { if (!this.gdeEnabled) { throw new McpError(ErrorCode.InvalidRequest, "GDE tools are not enabled. Please configure GDE credentials."); } switch (name) { case "submit_gde_content_creation": return await this.submitGDEActivityDraft("content-creation", args as unknown as ContentCreationDraft); // Add other GDE handlers... } } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } catch (error) { if (error instanceof McpError) { throw error; } const errorMsg = this.getErrorMessage(error); throw new McpError( ErrorCode.InternalError, `Failed to execute tool '${name}': ${errorMsg}` ); } }); } private setupResourceHandlers() { // List available documentation resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: DOCUMENTATION_RESOURCES.map((doc) => ({ uri: doc.uri, name: doc.name, description: doc.description, mimeType: doc.mimeType, })), }; }); // Read a specific documentation resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; // Find the requested resource const resource = DOCUMENTATION_RESOURCES.find((doc) => doc.uri === uri); if (!resource) { throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`); } try { // Read the file from the docs directory const filePath = join(DOCS_DIR, resource.file); const content = await readFile(filePath, "utf-8"); return { contents: [ { uri: resource.uri, mimeType: resource.mimeType, text: content, }, ], }; } catch (error) { const errorMsg = this.getErrorMessage(error); throw new McpError(ErrorCode.InternalError, `Failed to read resource '${resource.name}': ${errorMsg}`); } }); } // Documentation Tools private async listDocumentation() { const docsList = DOCUMENTATION_RESOURCES.map((doc) => ({ name: doc.uri.replace("docs://", ""), title: doc.name, description: doc.description, })); return { content: [ { type: "text", text: `📚 Available Documentation:\n\n${docsList .map((doc, i) => `${i + 1}. **${doc.title}** (${doc.name})\n ${doc.description}`) .join("\n\n")}`, }, ], }; } private async getDocumentation(args: { documentName: string }) { const { documentName } = args; const uri = `docs://${documentName}`; // Find the resource const resource = DOCUMENTATION_RESOURCES.find((doc) => doc.uri === uri); if (!resource) { return { content: [ { type: "text", text: `❌ Documentation not found: ${documentName}\n\nAvailable docs: ${DOCUMENTATION_RESOURCES.map((d) => d.uri.replace("docs://", "")).join(", ")}`, }, ], }; } try { // Read the file const filePath = join(DOCS_DIR, resource.file); const content = await readFile(filePath, "utf-8"); return { content: [ { type: "text", text: `# ${resource.name}\n\n${content}`, }, ], }; } catch (error) { const errorMsg = this.getErrorMessage(error); return { content: [ { type: "text", text: `❌ Failed to read ${resource.name}: ${errorMsg}`, }, ], }; } } // MVP Methods private async submitMVPVideoActivity(data: Partial<MVPVideoActivity>) { const activity: MVPVideoActivity = { id: 0, activityTypeName: MVPActivityType.VIDEO, typeName: "Video", date: new Date(data.date!).toISOString(), description: data.description!, isPrivate: data.isPrivate ?? false, targetAudience: data.targetAudience!, tenant: "MVP", title: data.title!, url: data.url!, userProfileId: this.mvpUserProfileId!, role: data.role!, technologyFocusArea: data.technologyFocusArea!, additionalTechnologyAreas: [], liveStreamViews: data.liveStreamViews!, onDemandViews: data.onDemandViews!, numberOfSessions: data.numberOfSessions ?? 1, inPersonAttendees: 0, subscriberBase: 0, imageUrl: "", }; return await this.submitMVPActivity(activity); } private async submitMVPBlogActivity(data: Partial<MVPBlogActivity>) { const activity: MVPBlogActivity = { id: 0, activityTypeName: MVPActivityType.BLOG, typeName: "Blog", date: new Date(data.date!).toISOString(), description: data.description!, isPrivate: data.isPrivate ?? false, targetAudience: data.targetAudience!, tenant: "MVP", title: data.title!, url: data.url!, userProfileId: this.mvpUserProfileId!, role: data.role!, technologyFocusArea: data.technologyFocusArea!, additionalTechnologyAreas: data.additionalTechnologyAreas, numberOfViews: data.numberOfViews!, subscriberBase: data.subscriberBase ?? 0, imageUrl: "", }; return await this.submitMVPActivity(activity); } private async submitMVPSpeakingActivity(data: Partial<MVPSpeakingActivity>) { const activity: MVPSpeakingActivity = { id: 0, activityTypeName: MVPActivityType.SPEAKING_CONFERENCE, typeName: "Speaking", date: new Date(data.date!).toISOString(), description: data.description!, isPrivate: data.isPrivate ?? false, targetAudience: data.targetAudience!, tenant: "MVP", title: data.title!, url: data.url!, userProfileId: this.mvpUserProfileId!, role: data.role!, technologyFocusArea: data.technologyFocusArea!, additionalTechnologyAreas: data.additionalTechnologyAreas, inPersonAttendees: data.inPersonAttendees!, numberOfSessions: data.numberOfSessions ?? 1, liveStreamViews: data.liveStreamViews ?? 0, onDemandViews: data.onDemandViews ?? 0, imageUrl: "", }; return await this.submitMVPActivity(activity); } private async submitMVPActivity(activity: MVPVideoActivity | MVPBlogActivity | MVPSpeakingActivity) { const url = `${this.mvpBaseUrl}/Activities/`; try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.mvpAccessToken}`, }, body: JSON.stringify({ activity }), }); if (!response.ok) { const errorText = await response.text(); let errorMessage = `MVP API error (${response.status})`; if (response.status === 401) { errorMessage = "❌ MVP authentication failed. Your MVP_ACCESS_TOKEN may be expired or invalid.\n\nTo fix:\n1. Log in to https://mvp.microsoft.com/\n2. Open DevTools → Network tab\n3. Create/edit an activity\n4. Export as HAR file\n5. Extract the Bearer token from Authorization header\n6. Update your configuration"; } else if (response.status === 400) { errorMessage = `❌ MVP API rejected the request:\n\n${errorText}\n\nPlease check:\n- Activity type is correct for the data provided\n- Role is valid for this activity type\n- All required fields are present\n- Date format is correct (YYYY-MM-DD)`; } else if (response.status === 429) { errorMessage = "⏱️ MVP API rate limit exceeded. Please wait a moment and try again."; } else { errorMessage = `❌ MVP API error (${response.status}):\n\n${errorText}`; } // Return error as content instead of throwing return { content: [ { type: "text", text: errorMessage, }, ], }; } const result = (await response.json()) as Record<string, unknown>; return { content: [ { type: "text", text: `✅ MVP Activity submitted!\n\nType: ${activity.activityTypeName}\nTitle: ${activity.title}\n\nResponse: ${JSON.stringify(result, null, 2)}`, }, ], }; } catch (error) { if (error instanceof McpError) throw error; const errorMsg = this.getErrorMessage(error); throw new McpError( ErrorCode.InternalError, `❌ Failed to submit MVP activity:\n\n${errorMsg}\n\nActivity type: ${activity.activityTypeName}\nTitle: ${activity.title}` ); } } // GDE Methods private async submitGDEActivityDraft(endpoint: string, data: ContentCreationDraft) { const url = `${this.gdeBaseUrl}/activity-drafts/${endpoint}`; try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.gdeAccessToken}`, }, body: JSON.stringify(data), }); if (!response.ok) { const errorText = await response.text(); let errorMessage = `GDE API error (${response.status})`; if (response.status === 401) { errorMessage = "❌ GDE authentication failed. Your ADVOCU_ACCESS_TOKEN may be expired or invalid.\n\nPlease check your Advocu access token configuration."; } else if (response.status === 400) { errorMessage = `❌ GDE API rejected the request:\n\n${errorText}\n\nPlease check:\n- All required fields are present\n- Field values match expected formats\n- Tags are valid\n- Date format is correct (YYYY-MM-DD)`; } else if (response.status === 429) { errorMessage = "⏱️ GDE API rate limit exceeded (30 requests/minute). Please wait and try again."; } else { errorMessage = `❌ GDE API error (${response.status}):\n\n${errorText}`; } // Return error as content instead of throwing return { content: [ { type: "text", text: errorMessage, }, ], }; } const result = (await response.json()) as Record<string, unknown>; return { content: [ { type: "text", text: `✅ GDE Activity submitted!\n\nEndpoint: ${endpoint}\n\nResponse: ${JSON.stringify(result, null, 2)}`, }, ], }; } catch (error) { if (error instanceof McpError) throw error; const errorMsg = this.getErrorMessage(error); throw new McpError( ErrorCode.InternalError, `❌ Failed to submit GDE activity:\n\n${errorMsg}\n\nEndpoint: ${endpoint}` ); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("🚀 Unified Activity Reporting MCP Server running"); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/carlosazaustre/advocu-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server