# Directory Structure
```
├── .gitignore
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── server.ts
│ ├── sse.ts
│ └── timezone-utils.ts
├── tests
│ └── timezone-utils.test.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependency directories
node_modules/
# Build output
dist/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Editor directories and files
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS specific
.DS_Store
Thumbs.db
# Test coverage
coverage/
.nyc_output/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# MCP DateTime
A TypeScript implementation of a Model Context Protocol (MCP) server that provides datetime and timezone information to agentic systems and chat REPLs.
## Overview
MCP DateTime is a simple server that implements the [Model Context Protocol](https://github.com/model-context-protocol/mcp) to provide datetime and timezone information to AI agents and chat interfaces. It allows AI systems to:
- Get the current time in the local system timezone
- Get the current time in any valid timezone
- List all available timezones
- Access timezone information through URI resources
## Installation
### From npm
```bash
npm install -g mcp-datetime
```
### From source
```bash
git clone https://github.com/odgrmi/mcp-datetime.git
cd mcp-datetime
npm install
npm run build
```
## Usage
### Command Line
MCP DateTime can be run in two modes:
#### 1. Standard I/O Mode (Default)
This mode is ideal for integrating with AI systems that support the MCP protocol through standard input/output:
```bash
mcp-datetime
```
#### 2. Server-Sent Events (SSE) Mode
This mode starts an HTTP server that provides SSE transport for the MCP protocol:
```bash
mcp-datetime --sse
```
You can also specify a custom port and URI prefix:
```bash
mcp-datetime --sse --port=8080 --prefix=/api/datetime
```
### Environment Variables
- `PORT`: Sets the port for SSE mode (default: 3000)
- `URI_PREFIX`: Sets the URI prefix for SSE mode (default: none)
## Available Tools
MCP DateTime provides the following tools:
### `get-current-time`
Returns the current time in the system's local timezone.
### `get-current-timezone`
Returns the current system timezone.
### `get-time-in-timezone`
Returns the current time in a specified timezone.
Parameters:
- `timezone`: The timezone to get the current time for (e.g., "America/New_York")
### `list-timezones`
Returns a list of all available timezones.
## Resource URIs
MCP DateTime also provides access to timezone information through resource URIs:
### `datetime://{timezone}`
Returns the current time in the specified timezone.
Example: `datetime://America/New_York`
### `datetime://list`
Returns a list of all available timezones.
## Common Timezones
The following common timezones are always available:
- UTC
- Europe/London
- Europe/Paris
- Europe/Berlin
- America/New_York
- America/Chicago
- America/Denver
- America/Los_Angeles
- Asia/Tokyo
- Asia/Shanghai
- Asia/Kolkata
- Australia/Sydney
- Pacific/Auckland
## SSE Endpoints
When running in SSE mode, the following endpoints are available:
- `/sse`: SSE connection endpoint
- `/message`: Message endpoint for client-to-server communication
- `/info`: Basic server information
If a URI prefix is specified, it will be prepended to all endpoints.
## Integration with AI Systems
MCP DateTime can be integrated with AI systems that support the Model Context Protocol. This allows AI agents to access accurate timezone and datetime information.
## Development
### Prerequisites
- Node.js 14.16 or higher
- npm
### Setup
```bash
git clone https://github.com/odgrim/mcp-datetime.git
cd mcp-datetime
npm install
```
### Build
```bash
npm run build
```
### Run in Development Mode
```bash
npm run dev # Standard I/O mode
npm run dev:sse # SSE mode
```
## License
This project is licensed under the Mozilla Public License 2.0 - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
```javascript
export default {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageReporters: ['text', 'lcov', 'clover', 'html'],
coverageDirectory: 'coverage',
};
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "@odgrim/mcp-datetime",
"version": "0.2.0",
"description": "A TypeScript implementation of a simple MCP server that exposes datetime information to agentic systems and chat REPLs",
"type": "module",
"main": "dist/index.js",
"bin": {
"mcp-datetime": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"start": "node dist/index.js",
"start:sse": "node dist/index.js --sse",
"dev": "ts-node --esm src/index.ts",
"dev:sse": "ts-node --esm src/index.ts --sse",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
},
"keywords": [
"mcp",
"datetime",
"timezone",
"model-context-protocol"
],
"author": "odgrim",
"license": "MPL-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"@types/express": "^5.0.0",
"express": "^4.21.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^20.11.24",
"jest": "^29.7.0",
"shx": "^0.3.4",
"ts-jest": "^29.2.6",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=14.16"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/odgrim/mcp-datetime.git"
},
"bugs": {
"url": "https://github.com/odgrim/mcp-datetime/issues"
},
"homepage": "https://github.com/odgrim/mcp-datetime#readme"
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { server } from "./server.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { startSSEServer } from "./sse.js";
async function main() {
console.error("Starting MCP DateTime server...");
// Check if the --sse flag is provided
const useSSE = process.argv.includes("--sse");
// Check if a custom port is provided with --port=XXXX
const portArg = process.argv.find(arg => arg.startsWith("--port="));
// Use PORT environment variable or command line argument or default to 3000
const defaultPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const port = portArg ? parseInt(portArg.split("=")[1], 10) : defaultPort;
// Check if a URI prefix is provided with --prefix=XXXX
const prefixArg = process.argv.find(arg => arg.startsWith("--prefix="));
// Use URI_PREFIX environment variable or command line argument or default to empty string
const defaultPrefix = process.env.URI_PREFIX || "";
const uriPrefix = prefixArg ? prefixArg.split("=")[1] : defaultPrefix;
try {
if (useSSE) {
// Start the SSE server
console.error(`Starting MCP DateTime server with SSE transport on port ${port}...`);
const cleanup = startSSEServer(port, uriPrefix);
// Handle graceful shutdown
process.on("SIGINT", async () => {
console.error("Received SIGINT, shutting down...");
await cleanup();
process.exit(0);
});
process.on("SIGTERM", async () => {
console.error("Received SIGTERM, shutting down...");
await cleanup();
process.exit(0);
});
} else {
// Connect to stdio transport
await server.connect(new StdioServerTransport());
console.error("MCP DateTime server connected to stdio transport");
}
} catch (error) {
console.error("Error starting MCP DateTime server:", error);
process.exit(1);
}
}
main().catch(error => {
console.error("Unhandled error:", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/sse.ts:
--------------------------------------------------------------------------------
```typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { server } from "./server.js";
/**
* Creates and starts an Express server that provides SSE transport for the MCP DateTime server
* @param port The port to listen on (defaults to PORT env var or 3000)
* @param uriPrefix The URI prefix to prepend to all routes (for reverse proxy scenarios)
* @returns A cleanup function to close the server
*/
export function startSSEServer(
port: number = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
uriPrefix: string = ""
): () => Promise<void> {
const app = express();
let transport: SSEServerTransport;
// Normalize the URI prefix to ensure it starts with a / and doesn't end with one
const normalizedPrefix = uriPrefix
? (uriPrefix.startsWith('/') ? uriPrefix : `/${uriPrefix}`).replace(/\/+$/, '')
: '';
// Define the endpoint paths with the prefix
const ssePath = `${normalizedPrefix}/sse`;
const messagePath = `${normalizedPrefix}/message`;
const infoPath = `${normalizedPrefix}/info`;
// SSE endpoint for establishing a connection
app.get(ssePath, async (req, res) => {
console.error("Received SSE connection");
transport = new SSEServerTransport(messagePath, res);
await server.connect(transport);
});
// Endpoint for receiving messages from the client
app.post(messagePath, async (req, res) => {
console.error("Received message");
await transport.handlePostMessage(req, res);
});
// Basic info endpoint
app.get(infoPath, (req, res) => {
res.json({
name: "MCP DateTime Server",
version: "0.1.0",
transport: "SSE",
endpoints: {
sse: ssePath,
message: messagePath,
info: infoPath
}
});
});
// Start the server
const httpServer = app.listen(port, () => {
console.error(`MCP DateTime server listening on port ${port}`);
console.error(`URI prefix: ${normalizedPrefix || '/'} (root)`);
console.error(`SSE endpoint: http://localhost:${port}${ssePath}`);
console.error(`Message endpoint: http://localhost:${port}${messagePath}`);
console.error(`Info endpoint: http://localhost:${port}${infoPath}`);
});
// Return a cleanup function
return async () => {
console.error("Closing SSE server...");
httpServer.close();
if (transport) {
await server.close();
}
};
}
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import {
COMMON_TIMEZONES,
getAvailableTimezones,
isValidTimezone,
getCurrentTimeInTimezone,
getCurrentTimezone,
getFormattedTimezoneList
} from "./timezone-utils.js";
// Create an MCP server
export const server = new McpServer({
name: "mcp-datetime",
version: "0.1.0"
});
// Add a tool to get the current time in the local timezone
server.tool(
"get-current-time",
"Get the current time in the configured local timezone",
async () => ({
content: [{
type: "text",
text: `The current time is ${getCurrentTimeInTimezone(getCurrentTimezone())}`
}]
})
);
// Add a tool to get the current system timezone
server.tool(
"get-current-timezone",
"Get the current system timezone",
async () => {
const timezone = getCurrentTimezone();
return {
content: [{
type: "text",
text: `The current system timezone is ${timezone}`
}]
};
}
);
// Add a tool to get the current time in a specific timezone
server.tool(
"get-time-in-timezone",
"Get the current time in a specific timezone",
{
timezone: z.string().describe("The timezone to get the current time for")
},
async (args) => {
if (!isValidTimezone(args.timezone)) {
return {
content: [{
type: "text",
text: `Error: Invalid timezone "${args.timezone}". Use the "list-timezones" tool to see available options.`
}],
isError: true
};
}
return {
content: [{
type: "text",
text: `The current time in ${args.timezone} is ${getCurrentTimeInTimezone(args.timezone)}`
}]
};
}
);
// Add a tool to list all available timezones
server.tool(
"list-timezones",
"List all available timezones",
async () => {
return {
content: [{
type: "text",
text: getFormattedTimezoneList()
}]
};
}
);
// Create a resource template for datetime URIs
// The list method is defined to return a list of common timezones
// to avoid overwhelming the client with all available timezones
const datetimeTemplate = new ResourceTemplate("datetime://{timezone}", {
list: async () => {
return {
resources: COMMON_TIMEZONES.map(timezone => ({
uri: `datetime://${encodeURIComponent(timezone)}`,
name: `Current time in ${timezone}`,
description: `Get the current time in the ${timezone} timezone`,
mimeType: "text/plain"
}))
};
}
});
// Register the template with the server
server.resource(
"datetime-template",
datetimeTemplate,
async (uri, variables) => {
// Decode the timezone from the URI
const encodedTimezone = variables.timezone as string;
const timezone = decodeURIComponent(encodedTimezone);
if (!timezone || !isValidTimezone(timezone)) {
throw new Error(`Invalid timezone: ${timezone}`);
}
const formattedTime = getCurrentTimeInTimezone(timezone);
return {
contents: [{
uri: decodeURIComponent(uri.href),
text: `Current time in ${timezone}: ${formattedTime}`,
mimeType: "text/plain"
}]
};
}
);
// Add a resource to list all available timezones
server.resource(
"datetime-list",
"datetime://list",
async () => {
return {
contents: [{
uri: "datetime://list",
text: getFormattedTimezoneList("All available timezones"),
mimeType: "text/plain"
}]
};
}
);
```
--------------------------------------------------------------------------------
/src/timezone-utils.ts:
--------------------------------------------------------------------------------
```typescript
// Common timezones to ensure they're always available
export const COMMON_TIMEZONES = [
"UTC",
"Europe/London",
"Europe/Paris",
"Europe/Berlin",
"America/New_York",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"Asia/Tokyo",
"Asia/Shanghai",
"Asia/Kolkata",
"Australia/Sydney",
"Pacific/Auckland"
];
/**
* Get all available timezones using the Intl API
* @returns Array of timezone strings
*/
export function getAvailableTimezones(): string[] {
try {
const timezones = new Set<string>(Intl.supportedValuesOf('timeZone'));
// Ensure common timezones are always included
COMMON_TIMEZONES.forEach(tz => timezones.add(tz));
return Array.from(timezones).sort();
} catch (error) {
console.error("Error getting timezones from Intl API:", error);
// Fallback to common timezones if the Intl API fails
return COMMON_TIMEZONES;
}
}
/**
* Format the list of available timezones as a string
* @param prefix Optional prefix text to include before the list (default: "Available timezones")
* @returns Formatted string with timezone count and comma-separated list
*/
export function getFormattedTimezoneList(prefix: string = "Available timezones"): string {
const timezones = getAvailableTimezones();
return `${prefix} (${timezones.length}): ${timezones.join(', ')}`;
}
/**
* Check if a timezone is valid
* @param timezone Timezone string to validate
* @returns boolean indicating if the timezone is valid
*/
export function isValidTimezone(timezone: string): boolean {
try {
// Try to use the timezone with Intl.DateTimeFormat
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
} catch (error) {
return false;
}
}
/**
* Get the current system timezone
* @returns The current system timezone string, or "UTC" as fallback
*/
export function getCurrentTimezone(): string {
try {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return processTimezone(timezone);
} catch (error) {
console.error("Error getting current timezone:", error);
return "UTC"; // Default to UTC if there's an error
}
}
// Export this function to make it testable
export function processTimezone(timezone: string): string {
// Verify it's a valid timezone
if (isValidTimezone(timezone)) {
return timezone;
}
return handleInvalidTimezone(timezone);
}
// Export this function to make it testable
export function handleInvalidTimezone(timezone: string): string {
console.warn(`System timezone ${timezone} is not valid, falling back to UTC`);
return "UTC";
}
/**
* Format the current date and time for a given timezone in ISO8601 format
* @param timezone Timezone string
* @returns Formatted date-time string in ISO8601 format
*/
export function getCurrentTimeInTimezone(timezone: string): string {
try {
const date = new Date();
// Create a formatter that includes the timezone
const options: Intl.DateTimeFormatOptions = {
timeZone: timezone,
timeZoneName: 'short'
};
// Get the timezone offset from the formatter
const formatter = new Intl.DateTimeFormat('en-US', options);
const formattedDate = formatter.format(date);
const timezonePart = formattedDate.split(' ').pop() || '';
// Format the date in ISO8601 format with the timezone
// First get the date in the specified timezone
const tzFormatter = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
fractionalSecondDigits: 3
});
const parts = tzFormatter.formatToParts(date);
const dateParts: Record<string, string> = {};
parts.forEach(part => {
if (part.type !== 'literal') {
dateParts[part.type] = part.value;
}
});
// Format as YYYY-MM-DDTHH:MM:SS.sss±HH:MM (ISO8601)
const isoDate = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}.${dateParts.fractionalSecond || '000'}`;
// For proper ISO8601, we need to add the timezone offset
// We can use the Intl.DateTimeFormat to get the timezone offset
const tzOffset = new Date().toLocaleString('en-US', { timeZone: timezone, timeZoneName: 'longOffset' }).split(' ').pop() || '';
// Format the final ISO8601 string
return `${isoDate}${tzOffset.replace('GMT', '')}`;
} catch (error) {
console.error(`Error formatting time for timezone ${timezone}:`, error);
return 'Invalid timezone';
}
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2022",
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "NodeNext",
"moduleResolution": "NodeNext",
// "rootDir": "./", /* Specify the root folder within your source files. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist",
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/tests/timezone-utils.test.ts:
--------------------------------------------------------------------------------
```typescript
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import * as tzUtils from '../src/timezone-utils.js';
import {
getAvailableTimezones,
getFormattedTimezoneList,
isValidTimezone,
getCurrentTimezone,
getCurrentTimeInTimezone,
handleInvalidTimezone,
processTimezone,
COMMON_TIMEZONES
} from '../src/timezone-utils.js';
describe('timezone-utils', () => {
// Store original methods to restore after tests
const originalSupportedValuesOf = Intl.supportedValuesOf;
const originalDateTimeFormat = Intl.DateTimeFormat;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
const originalToLocaleString = Date.prototype.toLocaleString;
// Restore original methods after each test
afterEach(() => {
Intl.supportedValuesOf = originalSupportedValuesOf;
Intl.DateTimeFormat = originalDateTimeFormat;
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
Date.prototype.toLocaleString = originalToLocaleString;
// Restore any mocked functions
jest.restoreAllMocks();
});
// Helper function to create a mock formatter
const createMockFormatter = (options: {
formattedDate?: string;
timeZone?: string;
timeZoneValue?: string;
includeFractionalSecond?: boolean;
} = {}) => {
const {
formattedDate = '2023-01-01T12:00:00.000+00:00',
timeZone = 'UTC',
timeZoneValue = 'GMT+00:00',
includeFractionalSecond = true
} = options;
const parts = [
{ type: 'year', value: '2023' },
{ type: 'literal', value: '-' },
{ type: 'month', value: '01' },
{ type: 'literal', value: '-' },
{ type: 'day', value: '01' },
{ type: 'literal', value: 'T' },
{ type: 'hour', value: '12' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '00' },
{ type: 'literal', value: ':' },
{ type: 'second', value: '00' },
{ type: 'literal', value: '.' }
];
if (includeFractionalSecond) {
parts.push({ type: 'fractionalSecond', value: '000' });
}
parts.push({ type: 'timeZoneName', value: timeZoneValue });
return {
format: () => formattedDate,
formatToParts: () => parts,
resolvedOptions: () => ({ timeZone })
};
};
// Helper function to mock DateTimeFormat
const mockDateTimeFormat = (formatter: any) => {
// @ts-ignore - TypeScript doesn't like us mocking built-in objects
Intl.DateTimeFormat = jest.fn().mockImplementation(() => formatter);
};
describe('getAvailableTimezones', () => {
it('should return a sorted array of timezones', () => {
const timezones = getAvailableTimezones();
// Check that we have an array of strings
expect(Array.isArray(timezones)).toBe(true);
expect(timezones.length).toBeGreaterThan(0);
// Check that all common timezones are included
COMMON_TIMEZONES.forEach(tz => {
expect(timezones).toContain(tz);
});
// Check that the array is sorted
const sortedTimezones = [...timezones].sort();
expect(timezones).toEqual(sortedTimezones);
});
it('should fall back to common timezones if Intl API fails', () => {
// Mock the Intl.supportedValuesOf to throw an error
Intl.supportedValuesOf = function() {
throw new Error('API not available');
} as any;
const timezones = getAvailableTimezones();
// Should fall back to common timezones
expect(timezones).toEqual(COMMON_TIMEZONES);
});
});
describe('getFormattedTimezoneList', () => {
it('should format the timezone list with default prefix', () => {
const timezones = getAvailableTimezones();
const formattedList = getFormattedTimezoneList();
expect(formattedList).toContain('Available timezones');
expect(formattedList).toContain(`(${timezones.length})`);
});
it('should format the timezone list with custom prefix', () => {
const timezones = getAvailableTimezones();
const customPrefix = 'Custom prefix';
const formattedList = getFormattedTimezoneList(customPrefix);
expect(formattedList).toContain(customPrefix);
expect(formattedList).toContain(`(${timezones.length})`);
});
});
describe('isValidTimezone', () => {
it('should return true for valid timezones', () => {
expect(isValidTimezone('UTC')).toBe(true);
expect(isValidTimezone('Europe/London')).toBe(true);
});
it('should return false for invalid timezones', () => {
expect(isValidTimezone('Invalid/Timezone')).toBe(false);
expect(isValidTimezone('')).toBe(false);
});
});
describe('getCurrentTimezone', () => {
beforeEach(() => {
console.warn = jest.fn();
console.error = jest.fn();
});
it('should return the current timezone', () => {
const timezone = getCurrentTimezone();
expect(typeof timezone).toBe('string');
expect(timezone.length).toBeGreaterThan(0);
});
it('should fall back to UTC if there is an error', () => {
// Mock Intl.DateTimeFormat to throw an error
Intl.DateTimeFormat = jest.fn().mockImplementation(() => {
throw new Error('API error');
}) as any;
const timezone = getCurrentTimezone();
expect(timezone).toBe('UTC');
expect(console.error).toHaveBeenCalledWith(
'Error getting current timezone:',
expect.any(Error)
);
});
it('should log a warning and fall back to UTC for invalid timezones', () => {
const invalidTimezone = 'Invalid/Timezone';
const result = handleInvalidTimezone(invalidTimezone);
expect(result).toBe('UTC');
expect(console.warn).toHaveBeenCalledWith(
`System timezone ${invalidTimezone} is not valid, falling back to UTC`
);
});
it('should call handleInvalidTimezone for invalid system timezone', () => {
// Create a function that simulates getCurrentTimezone with an invalid timezone
const simulateGetCurrentTimezoneWithInvalidTimezone = () => {
try {
const timezone = 'Invalid/Timezone';
if (isValidTimezone(timezone)) {
return timezone;
} else {
return handleInvalidTimezone(timezone);
}
} catch (error) {
console.error("Error getting current timezone:", error);
return "UTC";
}
};
const timezone = simulateGetCurrentTimezoneWithInvalidTimezone();
expect(timezone).toBe('UTC');
expect(console.warn).toHaveBeenCalledWith(
'System timezone Invalid/Timezone is not valid, falling back to UTC'
);
});
});
describe('getCurrentTimeInTimezone', () => {
beforeEach(() => {
console.error = jest.fn();
});
it('should format the current time in UTC', () => {
// Mock the DateTimeFormat constructor with our helper
mockDateTimeFormat(createMockFormatter());
const time = getCurrentTimeInTimezone('UTC');
// Check that it returns a string
expect(typeof time).toBe('string');
// Check that it's not the error message
expect(time).not.toBe('Invalid timezone');
});
it('should format the current time in a specific timezone', () => {
// Mock the DateTimeFormat constructor with our helper
mockDateTimeFormat(createMockFormatter({
formattedDate: '2023-01-01T12:00:00.000+01:00',
timeZone: 'Europe/London',
timeZoneValue: 'GMT+01:00'
}));
const time = getCurrentTimeInTimezone('Europe/London');
// Check that it returns a string
expect(typeof time).toBe('string');
// Check that it's not the error message
expect(time).not.toBe('Invalid timezone');
});
it('should return an error message for invalid timezones', () => {
const time = getCurrentTimeInTimezone('Invalid/Timezone');
expect(time).toBe('Invalid timezone');
});
it('should handle edge cases in date formatting', () => {
// Mock DateTimeFormat to throw an error
Intl.DateTimeFormat = jest.fn().mockImplementation(() => {
throw new Error('Mock error');
}) as any;
// This should trigger the catch block in getCurrentTimeInTimezone
const result = getCurrentTimeInTimezone('UTC');
// Verify we got the error message
expect(result).toBe('Invalid timezone');
// Verify console.error was called
expect(console.error).toHaveBeenCalled();
});
it('should handle empty timezone parts in formatting', () => {
// First mock for the formatter that gets the timezone offset
const mockEmptyFormatter = {
format: jest.fn().mockReturnValue('2023-01-01') // No timezone part
};
// Second mock for the formatter that gets the date parts
const mockTzFormatter = {
formatToParts: jest.fn().mockReturnValue([
{ type: 'year', value: '2023' },
{ type: 'month', value: '01' },
{ type: 'day', value: '01' },
{ type: 'hour', value: '12' },
{ type: 'minute', value: '00' },
{ type: 'second', value: '00' }
// No fractionalSecond to test that case
])
};
// Mock Date.toLocaleString to return a string without timezone
Date.prototype.toLocaleString = jest.fn().mockReturnValue('January 1, 2023') as any;
// Mock DateTimeFormat to return our formatters
Intl.DateTimeFormat = jest.fn()
.mockImplementationOnce(() => mockEmptyFormatter)
.mockImplementationOnce(() => mockTzFormatter) as any;
const result = getCurrentTimeInTimezone('UTC');
// Verify the result contains the expected date format
expect(result).toContain('2023-01-01T12:00:00.000');
});
it('should handle null values in split operations', () => {
// Mock formatter with null format result
const mockNullFormatter = {
format: jest.fn().mockReturnValue(null)
};
// Mock DateTimeFormat to return our formatter
Intl.DateTimeFormat = jest.fn().mockImplementation(() => mockNullFormatter) as any;
const result = getCurrentTimeInTimezone('UTC');
// The function should handle the null values and return an error
expect(result).toBe('Invalid timezone');
// Verify console.error was called
expect(console.error).toHaveBeenCalled();
});
it('should handle empty result from formatter.format()', () => {
// Mock for the formatter that returns empty string
const mockEmptyFormatter = {
format: jest.fn().mockReturnValue('')
};
// Mock for the formatter that gets the date parts
const mockTzFormatter = {
formatToParts: jest.fn().mockReturnValue([
{ type: 'year', value: '2023' },
{ type: 'month', value: '01' },
{ type: 'day', value: '01' },
{ type: 'hour', value: '12' },
{ type: 'minute', value: '00' },
{ type: 'second', value: '00' },
{ type: 'fractionalSecond', value: '123' }
])
};
// Mock Date.toLocaleString to return a valid string
Date.prototype.toLocaleString = jest.fn().mockReturnValue('1/1/2023, 12:00:00 PM GMT+0000') as any;
// Mock DateTimeFormat to return our formatters
Intl.DateTimeFormat = jest.fn()
.mockImplementationOnce(() => mockEmptyFormatter)
.mockImplementationOnce(() => mockTzFormatter) as any;
const result = getCurrentTimeInTimezone('UTC');
// The function should handle the empty string and still return a result
expect(result).toContain('2023-01-01T12:00:00.123');
});
it('should handle empty result from toLocaleString()', () => {
// Mock for the formatter that returns valid string
const mockFormatter = {
format: jest.fn().mockReturnValue('1/1/2023, 12:00:00 PM GMT+0000')
};
// Mock for the formatter that gets the date parts
const mockTzFormatter = {
formatToParts: jest.fn().mockReturnValue([
{ type: 'year', value: '2023' },
{ type: 'month', value: '01' },
{ type: 'day', value: '01' },
{ type: 'hour', value: '12' },
{ type: 'minute', value: '00' },
{ type: 'second', value: '00' },
{ type: 'fractionalSecond', value: '123' }
])
};
// Mock Date.toLocaleString to return an empty string
Date.prototype.toLocaleString = jest.fn().mockReturnValue('') as any;
// Mock DateTimeFormat to return our formatters
Intl.DateTimeFormat = jest.fn()
.mockImplementationOnce(() => mockFormatter)
.mockImplementationOnce(() => mockTzFormatter) as any;
const result = getCurrentTimeInTimezone('UTC');
// The function should handle the empty string and still return a result
expect(result).toContain('2023-01-01T12:00:00.123');
});
});
describe('handleInvalidTimezone', () => {
beforeEach(() => {
console.warn = jest.fn();
});
it('should log a warning and return UTC', () => {
const invalidTimezone = 'Invalid/Timezone';
const result = handleInvalidTimezone(invalidTimezone);
expect(result).toBe('UTC');
expect(console.warn).toHaveBeenCalledWith(
`System timezone ${invalidTimezone} is not valid, falling back to UTC`
);
});
it('should be called when isValidTimezone returns false', () => {
// Create a test function that simulates the exact code path
const testInvalidTimezone = (timezone: string) => {
if (isValidTimezone(timezone)) {
return timezone;
}
return handleInvalidTimezone(timezone);
};
// Use a timezone that we know is invalid
const result = testInvalidTimezone('Invalid/Timezone');
expect(result).toBe('UTC');
expect(console.warn).toHaveBeenCalledWith(
'System timezone Invalid/Timezone is not valid, falling back to UTC'
);
});
});
describe('processTimezone', () => {
beforeEach(() => {
console.warn = jest.fn();
});
it('should return the timezone if it is valid', () => {
const validTimezone = 'UTC';
const result = processTimezone(validTimezone);
expect(result).toBe(validTimezone);
expect(console.warn).not.toHaveBeenCalled();
});
it('should call handleInvalidTimezone for invalid timezones', () => {
const invalidTimezone = 'Invalid/Timezone';
const result = processTimezone(invalidTimezone);
expect(result).toBe('UTC');
expect(console.warn).toHaveBeenCalledWith(
`System timezone ${invalidTimezone} is not valid, falling back to UTC`
);
});
});
});
```