Skip to content

Commit 23e80b4

Browse files
authored
Merge pull request #9 from alchemyplatform/cm/date-functions
Added additional date parsing on prompts
2 parents 35b0e2f + 814bbc9 commit 23e80b4

File tree

6 files changed

+244
-63
lines changed

6 files changed

+244
-63
lines changed

index.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
import { z } from "zod";
66
import { alchemyApi } from './api/alchemyApi.js';
7-
import toISO8601 from './utils/toISO8601.js';
87
import { convertTimestampToDate } from './utils/convertTimestampToDate.js';
98
import { convertWeiToEth } from './utils/convertWeiToEth.js';
9+
import { calculateDateRange, parseNaturalLanguageTimeFrame, toISO8601 } from './utils/dateUtils.js';
1010
const server = new McpServer({
1111
name: "alchemy-mcp-server",
1212
version: "0.1.0",
@@ -95,6 +95,49 @@ server.tool('fetchTokenPriceHistoryBySymbol', {
9595
}
9696
});
9797

98+
// Fetch token price history using various time frame formats or natural language
99+
server.tool('fetchTokenPriceHistoryByTimeFrame', {
100+
symbol: z.string().describe('The token symbol to query. e.g. "BTC" or "ETH"'),
101+
timeFrame: z.string().describe('Time frame like "last-week", "past-7d", "ytd", "last-month", etc. or use natural language like "last week"'),
102+
interval: z.string().default('1d').describe('The interval to query. e.g. "1d" or "1h"'),
103+
useNaturalLanguageProcessing: z.boolean().default(false).describe('If true, will interpret timeFrame as natural language'),
104+
}, async (params) => {
105+
try {
106+
// Process time frame - either directly or through NLP
107+
let timeFrame = params.timeFrame;
108+
if (params.useNaturalLanguageProcessing) {
109+
timeFrame = parseNaturalLanguageTimeFrame(params.timeFrame);
110+
}
111+
112+
// Calculate date range
113+
const { startDate, endDate } = calculateDateRange(timeFrame);
114+
115+
// Fetch the data
116+
const result = await alchemyApi.getTokenPriceHistoryBySymbol({
117+
symbol: params.symbol,
118+
startTime: startDate,
119+
endTime: endDate,
120+
interval: params.interval
121+
});
122+
123+
return {
124+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
125+
};
126+
} catch (error) {
127+
if (error instanceof Error) {
128+
console.error('Error in fetchTokenPriceHistoryByTimeFrame:', error);
129+
return {
130+
content: [{ type: "text", text: `Error: ${error.message}` }],
131+
isError: true
132+
};
133+
}
134+
return {
135+
content: [{ type: "text", text: 'Unknown error occurred' }],
136+
isError: true
137+
};
138+
}
139+
});
140+
98141
// || ** MultiChain Token API ** ||
99142

100143
// Fetch the current balance, price, and metadata of the tokens owned by specific addresses using network and address pairs.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@alchemy/mcp-server",
3-
"version": "0.1.4",
3+
"version": "0.1.5",
44
"description": "MCP server for using Alchemy APIs",
55
"license": "MIT",
66
"author": "Alchemy, (https://www.alchemy.com)",

types/types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,9 @@ export interface NftContractsByAddressParams {
7373
export interface AddressPair {
7474
address: string;
7575
networks: string[];
76+
}
77+
78+
export interface DateRange {
79+
startDate: string;
80+
endDate: string;
7681
}

utils/dateUtils.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { DateRange } from "../types/types.js";
2+
3+
/**
4+
* Calculates date range based on a time frame string
5+
* @param timeFrame Various time frame formats like "last-week", "past-7d", "previous-calendar-week", "ytd", etc.
6+
* @returns Object with startDate and endDate in ISO 8601 format
7+
*/
8+
export function calculateDateRange(timeFrame: string): DateRange {
9+
// If timeFrame is a simple keyword that toISO8601 can handle directly
10+
if (['today', 'yesterday', 'last-week', 'last-month', 'start-of-year'].includes(timeFrame.toLowerCase())) {
11+
return {
12+
startDate: toISO8601(timeFrame),
13+
endDate: toISO8601('now')
14+
};
15+
}
16+
17+
const now = new Date();
18+
let startDate = new Date(now);
19+
let endDate = new Date(now);
20+
21+
const normalizedTimeFrame = timeFrame.toLowerCase();
22+
23+
// Handle special cases first
24+
if (normalizedTimeFrame === 'previous-calendar-week') {
25+
const today = now.getDay(); // 0 is Sunday, 6 is Saturday
26+
27+
// Calculate previous week's Sunday
28+
startDate = new Date(now);
29+
startDate.setDate(now.getDate() - today - 7);
30+
startDate.setHours(0, 0, 0, 0);
31+
32+
// Calculate previous week's Saturday
33+
endDate = new Date(startDate);
34+
endDate.setDate(startDate.getDate() + 6);
35+
endDate.setHours(23, 59, 59, 999);
36+
}
37+
// Handle trailing periods (last 7 days, etc.)
38+
else if (normalizedTimeFrame === 'last-7-days' || normalizedTimeFrame === 'last-week') {
39+
startDate.setDate(now.getDate() - 7);
40+
}
41+
// Handle past-Nd format (days)
42+
else if (normalizedTimeFrame.startsWith('past-') && normalizedTimeFrame.endsWith('d')) {
43+
const days = parseInt(normalizedTimeFrame.substring(5, normalizedTimeFrame.length - 1));
44+
if (!isNaN(days)) {
45+
startDate.setDate(now.getDate() - days);
46+
}
47+
}
48+
// Handle past-Nw format (weeks)
49+
else if (normalizedTimeFrame.startsWith('past-') && normalizedTimeFrame.endsWith('w')) {
50+
const weeks = parseInt(normalizedTimeFrame.substring(5, normalizedTimeFrame.length - 1));
51+
if (!isNaN(weeks)) {
52+
startDate.setDate(now.getDate() - (weeks * 7));
53+
}
54+
}
55+
// Handle past-Nm format (months)
56+
else if (normalizedTimeFrame.startsWith('past-') && normalizedTimeFrame.endsWith('m')) {
57+
const months = parseInt(normalizedTimeFrame.substring(5, normalizedTimeFrame.length - 1));
58+
if (!isNaN(months)) {
59+
startDate.setMonth(now.getMonth() - months);
60+
}
61+
}
62+
// Handle past-Ny format (years)
63+
else if (normalizedTimeFrame.startsWith('past-') && normalizedTimeFrame.endsWith('y')) {
64+
const years = parseInt(normalizedTimeFrame.substring(5, normalizedTimeFrame.length - 1));
65+
if (!isNaN(years)) {
66+
startDate.setFullYear(now.getFullYear() - years);
67+
}
68+
}
69+
// Handle ytd (year to date)
70+
else if (normalizedTimeFrame === 'ytd') {
71+
startDate = new Date(now.getFullYear(), 0, 1); // January 1st of current year
72+
}
73+
// Handle qtd (quarter to date)
74+
else if (normalizedTimeFrame === 'qtd') {
75+
const quarter = Math.floor(now.getMonth() / 3);
76+
startDate = new Date(now.getFullYear(), quarter * 3, 1);
77+
}
78+
// Handle mtd (month to date)
79+
else if (normalizedTimeFrame === 'mtd') {
80+
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
81+
}
82+
// Handle wtd (week to date - starting from Sunday)
83+
else if (normalizedTimeFrame === 'wtd') {
84+
const day = now.getDay(); // 0 = Sunday, 6 = Saturday
85+
startDate.setDate(now.getDate() - day);
86+
}
87+
// Default to 7 days if format not recognized
88+
else {
89+
startDate.setDate(now.getDate() - 7);
90+
console.warn(`Unrecognized timeFrame format: ${timeFrame}. Defaulting to past 7 days.`);
91+
}
92+
93+
return {
94+
startDate: startDate.toISOString(),
95+
endDate: endDate.toISOString()
96+
};
97+
}
98+
99+
/**
100+
* Natural language processing for time periods
101+
* This function handles natural language queries and converts them to standardized time frame strings
102+
* @param query Natural language query like "what was the price last week" or "show me prices from last month"
103+
* @returns Standardized time frame string
104+
*/
105+
export function parseNaturalLanguageTimeFrame(query: string): string {
106+
const normalizedQuery = query.toLowerCase();
107+
108+
if (normalizedQuery.includes('last week') || normalizedQuery.includes('previous week')) {
109+
return 'last-week';
110+
}
111+
else if (normalizedQuery.includes('last month') || normalizedQuery.includes('previous month')) {
112+
return 'last-month';
113+
}
114+
else if (normalizedQuery.includes('yesterday')) {
115+
return 'yesterday';
116+
}
117+
else if (normalizedQuery.includes('last year') || normalizedQuery.includes('previous year')) {
118+
return 'past-1y';
119+
}
120+
else if (normalizedQuery.includes('this year') || normalizedQuery.includes('year to date') || normalizedQuery.includes('ytd')) {
121+
return 'ytd';
122+
}
123+
else if (normalizedQuery.includes('this month') || normalizedQuery.includes('month to date') || normalizedQuery.includes('mtd')) {
124+
return 'mtd';
125+
}
126+
else if (normalizedQuery.includes('this quarter') || normalizedQuery.includes('quarter to date') || normalizedQuery.includes('qtd')) {
127+
return 'qtd';
128+
}
129+
else if (normalizedQuery.includes('calendar week')) {
130+
return 'previous-calendar-week';
131+
}
132+
133+
// Default to last 7 days if no specific time frame mentioned
134+
return 'last-7-days';
135+
}
136+
137+
export function toISO8601(dateStr: string): string {
138+
// Handle keywords first
139+
const normalizedStr = dateStr.toLowerCase();
140+
141+
const now = new Date();
142+
143+
switch (normalizedStr) {
144+
case 'today': {
145+
const date = new Date(now);
146+
date.setHours(0, 0, 0, 0);
147+
return date.toISOString();
148+
}
149+
case 'yesterday': {
150+
const date = new Date(now);
151+
date.setDate(date.getDate() - 1);
152+
date.setHours(0, 0, 0, 0);
153+
return date.toISOString();
154+
}
155+
case 'last-week': {
156+
const date = new Date(now);
157+
date.setDate(date.getDate() - 7);
158+
date.setHours(0, 0, 0, 0);
159+
return date.toISOString();
160+
}
161+
case 'last-month': {
162+
const date = new Date(now);
163+
date.setMonth(date.getMonth() - 1);
164+
date.setHours(0, 0, 0, 0);
165+
return date.toISOString();
166+
}
167+
case 'start-of-year': {
168+
const date = new Date(now);
169+
date.setMonth(0, 1);
170+
date.setHours(0, 0, 0, 0);
171+
return date.toISOString();
172+
}
173+
case 'now': {
174+
return now.toISOString();
175+
}
176+
}
177+
178+
// If already in ISO format, return as is
179+
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/)) {
180+
return dateStr;
181+
}
182+
183+
// For any other date string, parse it normally
184+
try {
185+
const date = new Date(dateStr);
186+
return date.toISOString();
187+
} catch (error) {
188+
console.error('Error parsing date:', error);
189+
// Return current time if parsing fails
190+
return now.toISOString();
191+
}
192+
}

utils/toISO8601.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)