Skip to main content
Glama

Get Analytics

get_analytics

Generate analytics reports on contact submissions to analyze summary statistics, purpose breakdown, daily trends, and activity patterns for data-driven insights.

Instructions

Generate analytics reports on contact submissions including summary statistics, purpose breakdown, daily trends, and recent activity patterns

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
report_typeNoType of analytics report to generatesummary
date_fromNoStart date for custom period (YYYY-MM-DD format)
date_toNoEnd date for custom period (YYYY-MM-DD format)

Implementation Reference

  • The core handler function for the 'get_analytics' tool. It processes input parameters (report_type, date_from, date_to) and executes database queries using Drizzle ORM to generate various analytics reports (summary, purpose breakdown, daily trends, recent activity, custom period). Returns structured content blocks with formatted text.
    		async ({ report_type = "summary", date_from, date_to }) => {
    			try {
    				switch (report_type) {
    					case "summary": {
    						const purposeBreakdown = await db
    							.select({
    								purpose: contacts.purpose,
    								count: count(contacts.id),
    							})
    							.from(contacts)
    							.groupBy(contacts.purpose);
    
    						const total = purposeBreakdown.reduce((sum, item) => sum + item.count, 0);
    						const thirtyDaysAgo = Math.floor(
    							(Date.now() - 30 * 24 * 60 * 60 * 1000) / 1000,
    						);
    
    						const recentCount = await db
    							.select({ count: count(contacts.id) })
    							.from(contacts)
    							.where(gte(contacts.createdAt, new Date(thirtyDaysAgo * 1000)));
    
    						const breakdown = purposeBreakdown
    							.map((item) => `${item.purpose.replace("_", " ")}: ${item.count}`)
    							.join(", ");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Contact Analytics Summary
    
    Total Contacts: ${total}
    Recent (30 days): ${recentCount[0]?.count || 0}
    
    Purpose Breakdown: ${breakdown}
    
    Status: ${total > 0 ? "Active contact system" : "No contacts yet"}`,
    								},
    							],
    						};
    					}
    
    					case "purpose_breakdown": {
    						const result = await db
    							.select({
    								purpose: contacts.purpose,
    								count: count(contacts.id),
    							})
    							.from(contacts)
    							.groupBy(contacts.purpose);
    
    						const total = result.reduce((sum, item) => sum + item.count, 0);
    
    						const breakdown = result
    							.map((item) => {
    								const percentage =
    									total > 0 ? ((item.count / total) * 100).toFixed(1) : 0;
    
    								return `${item.purpose.replace("_", " ")}
    Count: ${item.count}
    Percentage: ${percentage}%`;
    							})
    							.join("\n\n");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Contact Purpose Breakdown
    
    Total Contacts: ${total}
    
    ${breakdown}`,
    								},
    							],
    						};
    					}
    
    					case "daily_trends": {
    						const result = await db
    							.select({
    								date: sql<string>`strftime('%Y-%m-%d', datetime(${contacts.createdAt}, 'unixepoch'))`,
    								daily_total: count(contacts.id),
    							})
    							.from(contacts)
    							.groupBy(
    								sql`strftime('%Y-%m-%d', datetime(${contacts.createdAt}, 'unixepoch'))`,
    							)
    							.orderBy(
    								sql`strftime('%Y-%m-%d', datetime(${contacts.createdAt}, 'unixepoch')) DESC`,
    							)
    							.limit(30);
    
    						const chart = result
    							.map((item) => {
    								const date = new Date(item.date!).toLocaleDateString();
    								return `${date}: ${item.daily_total} contacts`;
    							})
    							.join("\n");
    
    						const totalRecent = result.reduce((sum, item) => sum + item.daily_total, 0);
    						const avgDaily = (totalRecent / result.length).toFixed(1);
    						const maxValue = Math.max(...result.map((item) => item.daily_total));
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Daily Contact Trends (Last 30 Days)
    
    ${chart}
    
    Statistics
    - Total (30 days): ${totalRecent}
    - Daily Average: ${avgDaily}
    - Peak Day: ${maxValue} contacts
    - Most Recent: ${result[0]?.daily_total || 0} contacts`,
    								},
    							],
    						};
    					}
    
    					case "recent_activity": {
    						const sevenDaysAgo = Math.floor(
    							(Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000,
    						);
    						const result = await db
    							.select({
    								purpose: contacts.purpose,
    								total: count(contacts.id),
    								last_submission: sql<string>`max(${contacts.createdAt})`,
    							})
    							.from(contacts)
    							.where(gte(contacts.createdAt, new Date(sevenDaysAgo * 1000)))
    							.groupBy(contacts.purpose);
    
    						const total = result.reduce((sum, item) => sum + item.total, 0);
    
    						const activity = result
    							.map((item) => {
    								const lastDate = new Date(
    									item.last_submission!,
    								).toLocaleDateString();
    
    								return `${item.purpose.replace("_", " ")}
    - Submissions: ${item.total}
    - Last activity: ${lastDate}`;
    							})
    							.join("\n\n");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Recent Activity (Last 7 Days)
    
    Total Submissions: ${total}
    
    ${activity || "No recent activity"}
    
    Status: ${total > 0 ? "Active" : "Quiet period"}`,
    								},
    							],
    						};
    					}
    
    					case "custom_period": {
    						if (!date_from || !date_to) {
    							return {
    								content: [
    									{
    										type: "text",
    										text: `Missing Date Range
    										
    For custom period analytics, please provide both date_from and date_to parameters in YYYY-MM-DD format.
    
    Example: date_from: "2024-01-01", date_to: "2024-01-31"`,
    									},
    								],
    							};
    						}
    
    						const startDate = new Date(date_from);
    						const endDate = new Date(date_to);
    
    						if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
    							return {
    								content: [
    									{
    										type: "text",
    										text: "Invalid date format. Please use YYYY-MM-DD format.",
    									},
    								],
    							};
    						}
    
    						const result = await db
    							.select({
    								purpose: contacts.purpose,
    								count: count(contacts.id),
    							})
    							.from(contacts)
    							.where(
    								sql`${contacts.createdAt} >= ${Math.floor(startDate.getTime() / 1000)} AND ${contacts.createdAt} <= ${Math.floor(endDate.getTime() / 1000)}`,
    							)
    							.groupBy(contacts.purpose);
    
    						const total = result.reduce((sum, item) => sum + item.count, 0);
    
    						const breakdown = result
    							.map((item) => `${item.purpose.replace("_", " ")}: ${item.count}`)
    							.join("\n");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Custom Period Analytics (${date_from} to ${date_to})
    
    Total Contacts: ${total}
    
    Purpose Breakdown:
    ${breakdown || "No contacts in this period"}`,
    								},
    							],
    						};
    					}
    
    					default:
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Invalid Report Type
    									
    Supported report types:
    - summary
    - purpose_breakdown
    - daily_trends
    - recent_activity
    - custom_period`,
    								},
    							],
    						};
    				}
    			} catch (_error) {
    				// Log error securely without exposing details
    				return {
    					content: [
    						{
    							type: "text",
    							text: `Unexpected Error
    							
    An error occurred while generating analytics. Please try again later.`,
    						},
    					],
    				};
    			}
    		},
  • Zod-based input schema definitions for the tool parameters: report_type (enum of report types), date_from and date_to (optional strings for custom date ranges).
    const reportTypeSchema = z
    	.enum(["summary", "purpose_breakdown", "daily_trends", "recent_activity", "custom_period"])
    	.default("summary") as any;
    const dateFromSchema = z.string().optional() as any;
    const dateToSchema = z.string().optional() as any;
    
    import { getDb } from "../database/index";
    import { contacts } from "../database/schema";
    
    /**
     * Register the contact-analytics MCP tool for insights and statistics
     * Analytics data is written to Cloudflare Analytics Engine and computed from D1 contacts
     */
    export function registerGetAnalyticsTool(server: McpServer, env: Env) {
    	const db = getDb(env.DB);
    
    	server.registerTool(
    		"get_analytics",
    		{
    			title: "Get Analytics",
    			description:
    				"Generate analytics reports on contact submissions including summary statistics, purpose breakdown, daily trends, and recent activity patterns",
    			inputSchema: {
    				report_type: reportTypeSchema.describe("Type of analytics report to generate"),
    				date_from: dateFromSchema.describe(
    					"Start date for custom period (YYYY-MM-DD format)",
    				),
    				date_to: dateToSchema.describe("End date for custom period (YYYY-MM-DD format)"),
    			},
  • The registerGetAnalyticsTool function that calls server.registerTool to define and register the 'get_analytics' tool with its schema and handler.
    export function registerGetAnalyticsTool(server: McpServer, env: Env) {
    	const db = getDb(env.DB);
    
    	server.registerTool(
    		"get_analytics",
    		{
    			title: "Get Analytics",
    			description:
    				"Generate analytics reports on contact submissions including summary statistics, purpose breakdown, daily trends, and recent activity patterns",
    			inputSchema: {
    				report_type: reportTypeSchema.describe("Type of analytics report to generate"),
    				date_from: dateFromSchema.describe(
    					"Start date for custom period (YYYY-MM-DD format)",
    				),
    				date_to: dateToSchema.describe("End date for custom period (YYYY-MM-DD format)"),
    			},
    		},
    		async ({ report_type = "summary", date_from, date_to }) => {
    			try {
    				switch (report_type) {
    					case "summary": {
    						const purposeBreakdown = await db
    							.select({
    								purpose: contacts.purpose,
    								count: count(contacts.id),
    							})
    							.from(contacts)
    							.groupBy(contacts.purpose);
    
    						const total = purposeBreakdown.reduce((sum, item) => sum + item.count, 0);
    						const thirtyDaysAgo = Math.floor(
    							(Date.now() - 30 * 24 * 60 * 60 * 1000) / 1000,
    						);
    
    						const recentCount = await db
    							.select({ count: count(contacts.id) })
    							.from(contacts)
    							.where(gte(contacts.createdAt, new Date(thirtyDaysAgo * 1000)));
    
    						const breakdown = purposeBreakdown
    							.map((item) => `${item.purpose.replace("_", " ")}: ${item.count}`)
    							.join(", ");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Contact Analytics Summary
    
    Total Contacts: ${total}
    Recent (30 days): ${recentCount[0]?.count || 0}
    
    Purpose Breakdown: ${breakdown}
    
    Status: ${total > 0 ? "Active contact system" : "No contacts yet"}`,
    								},
    							],
    						};
    					}
    
    					case "purpose_breakdown": {
    						const result = await db
    							.select({
    								purpose: contacts.purpose,
    								count: count(contacts.id),
    							})
    							.from(contacts)
    							.groupBy(contacts.purpose);
    
    						const total = result.reduce((sum, item) => sum + item.count, 0);
    
    						const breakdown = result
    							.map((item) => {
    								const percentage =
    									total > 0 ? ((item.count / total) * 100).toFixed(1) : 0;
    
    								return `${item.purpose.replace("_", " ")}
    Count: ${item.count}
    Percentage: ${percentage}%`;
    							})
    							.join("\n\n");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Contact Purpose Breakdown
    
    Total Contacts: ${total}
    
    ${breakdown}`,
    								},
    							],
    						};
    					}
    
    					case "daily_trends": {
    						const result = await db
    							.select({
    								date: sql<string>`strftime('%Y-%m-%d', datetime(${contacts.createdAt}, 'unixepoch'))`,
    								daily_total: count(contacts.id),
    							})
    							.from(contacts)
    							.groupBy(
    								sql`strftime('%Y-%m-%d', datetime(${contacts.createdAt}, 'unixepoch'))`,
    							)
    							.orderBy(
    								sql`strftime('%Y-%m-%d', datetime(${contacts.createdAt}, 'unixepoch')) DESC`,
    							)
    							.limit(30);
    
    						const chart = result
    							.map((item) => {
    								const date = new Date(item.date!).toLocaleDateString();
    								return `${date}: ${item.daily_total} contacts`;
    							})
    							.join("\n");
    
    						const totalRecent = result.reduce((sum, item) => sum + item.daily_total, 0);
    						const avgDaily = (totalRecent / result.length).toFixed(1);
    						const maxValue = Math.max(...result.map((item) => item.daily_total));
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Daily Contact Trends (Last 30 Days)
    
    ${chart}
    
    Statistics
    - Total (30 days): ${totalRecent}
    - Daily Average: ${avgDaily}
    - Peak Day: ${maxValue} contacts
    - Most Recent: ${result[0]?.daily_total || 0} contacts`,
    								},
    							],
    						};
    					}
    
    					case "recent_activity": {
    						const sevenDaysAgo = Math.floor(
    							(Date.now() - 7 * 24 * 60 * 60 * 1000) / 1000,
    						);
    						const result = await db
    							.select({
    								purpose: contacts.purpose,
    								total: count(contacts.id),
    								last_submission: sql<string>`max(${contacts.createdAt})`,
    							})
    							.from(contacts)
    							.where(gte(contacts.createdAt, new Date(sevenDaysAgo * 1000)))
    							.groupBy(contacts.purpose);
    
    						const total = result.reduce((sum, item) => sum + item.total, 0);
    
    						const activity = result
    							.map((item) => {
    								const lastDate = new Date(
    									item.last_submission!,
    								).toLocaleDateString();
    
    								return `${item.purpose.replace("_", " ")}
    - Submissions: ${item.total}
    - Last activity: ${lastDate}`;
    							})
    							.join("\n\n");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Recent Activity (Last 7 Days)
    
    Total Submissions: ${total}
    
    ${activity || "No recent activity"}
    
    Status: ${total > 0 ? "Active" : "Quiet period"}`,
    								},
    							],
    						};
    					}
    
    					case "custom_period": {
    						if (!date_from || !date_to) {
    							return {
    								content: [
    									{
    										type: "text",
    										text: `Missing Date Range
    										
    For custom period analytics, please provide both date_from and date_to parameters in YYYY-MM-DD format.
    
    Example: date_from: "2024-01-01", date_to: "2024-01-31"`,
    									},
    								],
    							};
    						}
    
    						const startDate = new Date(date_from);
    						const endDate = new Date(date_to);
    
    						if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
    							return {
    								content: [
    									{
    										type: "text",
    										text: "Invalid date format. Please use YYYY-MM-DD format.",
    									},
    								],
    							};
    						}
    
    						const result = await db
    							.select({
    								purpose: contacts.purpose,
    								count: count(contacts.id),
    							})
    							.from(contacts)
    							.where(
    								sql`${contacts.createdAt} >= ${Math.floor(startDate.getTime() / 1000)} AND ${contacts.createdAt} <= ${Math.floor(endDate.getTime() / 1000)}`,
    							)
    							.groupBy(contacts.purpose);
    
    						const total = result.reduce((sum, item) => sum + item.count, 0);
    
    						const breakdown = result
    							.map((item) => `${item.purpose.replace("_", " ")}: ${item.count}`)
    							.join("\n");
    
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Custom Period Analytics (${date_from} to ${date_to})
    
    Total Contacts: ${total}
    
    Purpose Breakdown:
    ${breakdown || "No contacts in this period"}`,
    								},
    							],
    						};
    					}
    
    					default:
    						return {
    							content: [
    								{
    									type: "text",
    									text: `Invalid Report Type
    									
    Supported report types:
    - summary
    - purpose_breakdown
    - daily_trends
    - recent_activity
    - custom_period`,
    								},
    							],
    						};
    				}
    			} catch (_error) {
    				// Log error securely without exposing details
    				return {
    					content: [
    						{
    							type: "text",
    							text: `Unexpected Error
    							
    An error occurred while generating analytics. Please try again later.`,
    						},
    					],
    				};
    			}
    		},
    	);
    }
  • In the registerAllTools function, imports and invokes registerGetAnalyticsTool to register the tool as part of the overall MCP tools initialization.
    import { registerGetAnalyticsTool } from "./contact-analytics";
    
    /**
     * Register all MCP tools with the server
     */
    export function registerAllTools(server: McpServer, env: Env) {
    	logger.info("init", "Registering MCP tools");
    
    	// Content tools
    	registerGitHubActivityTool(server);
    	logger.tool("github_activity", "registered");
    	registerGetBlogPostContentTool(server);
    	logger.tool("get_blog_post_content", "registered");
    
    	// Web tools
    	registerWebSearchTool(server);
    	logger.tool("web-search", "registered");
    	registerWebFetchTool(server);
    	logger.tool("web-fetch", "registered");
    
    	// Interaction tools
    	registerSendMessageTool(server, env);
    	logger.tool("send_message", "registered");
    	registerHireMeTool(server, env);
    	logger.tool("hire_me", "registered");
    	registerSayHiTool(server);
    	logger.tool("say_hi", "registered");
    
    	// Management tools
    	registerGetAnalyticsTool(server, env);
    	logger.tool("get_analytics", "registered");
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. The description mentions what the tool generates but doesn't disclose behavioral traits like whether it requires authentication, has rate limits, returns data in a specific format, or involves any side effects. For a reporting tool with zero annotation coverage, this leaves significant gaps in understanding how it behaves.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose and lists key components. It avoids unnecessary words and gets straight to the point, though it could be slightly more structured by separating usage context from functionality.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (analytics generation with multiple report types) and lack of annotations and output schema, the description is incomplete. It doesn't cover behavioral aspects, return values, or detailed usage scenarios. The description alone is insufficient for an agent to fully understand how to invoke and interpret results from this tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description lists report components (summary statistics, purpose breakdown, daily trends, recent activity patterns) that partially map to the 'report_type' enum values, adding some meaning beyond the schema. However, with 100% schema description coverage, the schema already documents all parameters thoroughly. The description doesn't explain parameter interactions (e.g., that 'custom_period' requires date_from/date_to) or add significant semantic depth, so it meets the baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Generate analytics reports on contact submissions' with specific components listed (summary statistics, purpose breakdown, daily trends, recent activity patterns). It uses a specific verb ('Generate') and identifies the resource ('analytics reports on contact submissions'). However, it doesn't distinguish this tool from sibling tools, which are unrelated (blog posts, GitHub activity, hiring, messaging, web operations).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites, appropriate contexts, or exclusions. While sibling tools are unrelated, there's no explicit comparison or usage context provided beyond the basic function.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

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/duyet/duyet-mcp-server'

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