@findtime/mcp-server 3.25.1 → 3.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/server.js +168 -8
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -30,6 +30,7 @@ const DEFAULT_API_KEY = firstNonEmpty(
|
|
|
30
30
|
process.env.FINDTIME_MCP_API_KEY,
|
|
31
31
|
process.env.FINDTIME_TIME_API_KEY
|
|
32
32
|
);
|
|
33
|
+
const TIMEZONE_HELPERS_PATH = path.join(REPO_ROOT, 'slack-bot', 'timezone-helpers.js');
|
|
33
34
|
|
|
34
35
|
const TOOL_DEFINITIONS = [
|
|
35
36
|
{
|
|
@@ -72,7 +73,7 @@ const TOOL_DEFINITIONS = [
|
|
|
72
73
|
},
|
|
73
74
|
{
|
|
74
75
|
name: 'get_current_time',
|
|
75
|
-
description: 'Return the production current time payload for a single city, query, or timezone.',
|
|
76
|
+
description: 'Return the production current time payload for a single city, query, or timezone. Exact country-name queries may be retried through a canonical city when the resolver can do so deterministically.',
|
|
76
77
|
inputSchema: {
|
|
77
78
|
type: 'object',
|
|
78
79
|
properties: {
|
|
@@ -82,7 +83,7 @@ const TOOL_DEFINITIONS = [
|
|
|
82
83
|
},
|
|
83
84
|
query: {
|
|
84
85
|
type: 'string',
|
|
85
|
-
description: 'Free-form location query or timezone abbreviation.'
|
|
86
|
+
description: 'Free-form location query, country name, or timezone abbreviation.'
|
|
86
87
|
},
|
|
87
88
|
timezone: {
|
|
88
89
|
type: 'string',
|
|
@@ -260,7 +261,7 @@ const TOOL_DEFINITIONS = [
|
|
|
260
261
|
},
|
|
261
262
|
countryCode: {
|
|
262
263
|
type: 'string',
|
|
263
|
-
description: 'Optional ISO country hint.'
|
|
264
|
+
description: 'Optional ISO country hint used for ranking and disambiguation. This is not a strict filter.'
|
|
264
265
|
},
|
|
265
266
|
limit: {
|
|
266
267
|
type: 'integer',
|
|
@@ -305,6 +306,7 @@ const TOOL_DEFINITIONS = [
|
|
|
305
306
|
];
|
|
306
307
|
|
|
307
308
|
const TOOL_DEFINITIONS_BY_NAME = new Map(TOOL_DEFINITIONS.map((tool) => [tool.name, tool]));
|
|
309
|
+
let cachedResolveLocation;
|
|
308
310
|
|
|
309
311
|
function safeReadJson(filePath) {
|
|
310
312
|
try {
|
|
@@ -564,7 +566,7 @@ function summarizeToolPayload(toolName, payload) {
|
|
|
564
566
|
return `${toolName} response\n${JSON.stringify(payload, null, 2)}`;
|
|
565
567
|
}
|
|
566
568
|
|
|
567
|
-
function buildToolErrorResult(toolName, apiResponse) {
|
|
569
|
+
function buildToolErrorResult(toolName, apiResponse, options = {}) {
|
|
568
570
|
const summary = {
|
|
569
571
|
ok: false,
|
|
570
572
|
tool: toolName,
|
|
@@ -577,6 +579,10 @@ function buildToolErrorResult(toolName, apiResponse) {
|
|
|
577
579
|
summary.networkError = apiResponse.networkError;
|
|
578
580
|
}
|
|
579
581
|
|
|
582
|
+
if (options.hint) {
|
|
583
|
+
summary.hint = options.hint;
|
|
584
|
+
}
|
|
585
|
+
|
|
580
586
|
return {
|
|
581
587
|
isError: true,
|
|
582
588
|
content: [
|
|
@@ -589,12 +595,14 @@ function buildToolErrorResult(toolName, apiResponse) {
|
|
|
589
595
|
};
|
|
590
596
|
}
|
|
591
597
|
|
|
592
|
-
function buildToolSuccessResult(toolName, payload, apiMeta) {
|
|
598
|
+
function buildToolSuccessResult(toolName, payload, apiMeta, options = {}) {
|
|
593
599
|
const structuredContent = {
|
|
594
600
|
...payload,
|
|
595
601
|
_meta: {
|
|
602
|
+
...(payload && typeof payload === 'object' && payload._meta ? payload._meta : {}),
|
|
596
603
|
endpoint: apiMeta.endpoint,
|
|
597
|
-
url: apiMeta.url
|
|
604
|
+
url: apiMeta.url,
|
|
605
|
+
...(options.meta || {})
|
|
598
606
|
}
|
|
599
607
|
};
|
|
600
608
|
|
|
@@ -609,6 +617,109 @@ function buildToolSuccessResult(toolName, payload, apiMeta) {
|
|
|
609
617
|
};
|
|
610
618
|
}
|
|
611
619
|
|
|
620
|
+
function loadResolveLocation() {
|
|
621
|
+
if (cachedResolveLocation !== undefined) {
|
|
622
|
+
return cachedResolveLocation;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
const timezoneHelpers = require(TIMEZONE_HELPERS_PATH);
|
|
627
|
+
cachedResolveLocation = typeof timezoneHelpers.resolveLocation === 'function'
|
|
628
|
+
? timezoneHelpers.resolveLocation
|
|
629
|
+
: null;
|
|
630
|
+
} catch (_error) {
|
|
631
|
+
cachedResolveLocation = null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return cachedResolveLocation;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizeCountryCode(value) {
|
|
638
|
+
return typeof value === 'string' && value.trim()
|
|
639
|
+
? value.trim().toUpperCase()
|
|
640
|
+
: null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function resolveCountryStyleInput(input, resolveLocationImpl) {
|
|
644
|
+
if (typeof resolveLocationImpl !== 'function') {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const resolved = resolveLocationImpl(String(input || '').trim());
|
|
649
|
+
const resolutionType = String(resolved && resolved.type ? resolved.type : '');
|
|
650
|
+
if (!resolved || !resolutionType.startsWith('country-')) {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const city = firstNonEmpty(resolved.city);
|
|
655
|
+
const timezone = firstNonEmpty(resolved.timezone);
|
|
656
|
+
const countryCode = normalizeCountryCode(resolved.countryCode);
|
|
657
|
+
|
|
658
|
+
if (!city && !timezone) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
city,
|
|
664
|
+
timezone,
|
|
665
|
+
countryCode,
|
|
666
|
+
resolutionType
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function buildCountryQueryHint(rawInput, resolvedCountry) {
|
|
671
|
+
if (!rawInput || !resolvedCountry) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const suggestions = [];
|
|
676
|
+
if (resolvedCountry.city) suggestions.push(resolvedCountry.city);
|
|
677
|
+
if (resolvedCountry.timezone && !suggestions.includes(resolvedCountry.timezone)) {
|
|
678
|
+
suggestions.push(resolvedCountry.timezone);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
reason: 'country_query_not_supported',
|
|
683
|
+
input: rawInput,
|
|
684
|
+
suggest: suggestions,
|
|
685
|
+
resolutionType: resolvedCountry.resolutionType,
|
|
686
|
+
retriedWith: {
|
|
687
|
+
city: resolvedCountry.city || null,
|
|
688
|
+
countryCode: resolvedCountry.countryCode || null,
|
|
689
|
+
timezone: resolvedCountry.timezone || null
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function enrichLocationSearchPayload(payload, args) {
|
|
695
|
+
if (!payload || typeof payload !== 'object') {
|
|
696
|
+
return { payload, meta: null };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const hintedCountryCode = normalizeCountryCode(args && args.countryCode);
|
|
700
|
+
if (!hintedCountryCode || !Array.isArray(payload.results)) {
|
|
701
|
+
return { payload, meta: null };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const countryFilteredResults = payload.results.filter(
|
|
705
|
+
(result) => normalizeCountryCode(result && result.countryCode) === hintedCountryCode
|
|
706
|
+
);
|
|
707
|
+
const primaryMatch = countryFilteredResults[0] || null;
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
payload: {
|
|
711
|
+
...payload,
|
|
712
|
+
primaryMatch,
|
|
713
|
+
countryFilteredResults
|
|
714
|
+
},
|
|
715
|
+
meta: {
|
|
716
|
+
countryCodeBehavior: 'ranking_hint',
|
|
717
|
+
countryHint: hintedCountryCode,
|
|
718
|
+
countryFilteredCount: countryFilteredResults.length
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
612
723
|
function createFindtimeMcpServer(options = {}) {
|
|
613
724
|
const fetchImpl = options.fetchImpl || global.fetch;
|
|
614
725
|
const apiBaseUrl = options.apiBaseUrl || DEFAULT_API_BASE_URL;
|
|
@@ -616,6 +727,9 @@ function createFindtimeMcpServer(options = {}) {
|
|
|
616
727
|
const apiKey = options.apiKey === undefined ? DEFAULT_API_KEY : options.apiKey;
|
|
617
728
|
const serverName = options.serverName || 'findtime';
|
|
618
729
|
const serverTitle = options.serverTitle || 'findtime Time API MCP';
|
|
730
|
+
const resolveLocationImpl = options.resolveLocationImpl === undefined
|
|
731
|
+
? loadResolveLocation()
|
|
732
|
+
: options.resolveLocationImpl;
|
|
619
733
|
|
|
620
734
|
if (typeof fetchImpl !== 'function') {
|
|
621
735
|
throw new Error('Fetch implementation is required to run the MCP server.');
|
|
@@ -694,15 +808,61 @@ function createFindtimeMcpServer(options = {}) {
|
|
|
694
808
|
}
|
|
695
809
|
|
|
696
810
|
const request = tool.buildRequest(args || {});
|
|
697
|
-
|
|
811
|
+
let apiResponse = await fetchJson(name, request);
|
|
698
812
|
|
|
699
813
|
if (!apiResponse.ok) {
|
|
814
|
+
if (name === 'get_current_time' && apiResponse.status === 404) {
|
|
815
|
+
const rawCountryQuery = firstNonEmpty(args.query, args.city);
|
|
816
|
+
const resolvedCountry = resolveCountryStyleInput(rawCountryQuery, resolveLocationImpl);
|
|
817
|
+
|
|
818
|
+
if (resolvedCountry && resolvedCountry.city) {
|
|
819
|
+
const retryRequest = tool.buildRequest({
|
|
820
|
+
city: resolvedCountry.city,
|
|
821
|
+
countryCode: resolvedCountry.countryCode || undefined
|
|
822
|
+
});
|
|
823
|
+
const retryResponse = await fetchJson(name, retryRequest);
|
|
824
|
+
|
|
825
|
+
if (retryResponse.ok) {
|
|
826
|
+
return buildToolSuccessResult(name, retryResponse.parsedBody, {
|
|
827
|
+
endpoint: retryRequest.path,
|
|
828
|
+
url: retryResponse.url
|
|
829
|
+
}, {
|
|
830
|
+
meta: {
|
|
831
|
+
fallbackResolution: {
|
|
832
|
+
input: rawCountryQuery,
|
|
833
|
+
strategy: resolvedCountry.resolutionType,
|
|
834
|
+
retriedWith: {
|
|
835
|
+
city: resolvedCountry.city,
|
|
836
|
+
countryCode: resolvedCountry.countryCode || null
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return buildToolErrorResult(name, retryResponse, {
|
|
844
|
+
hint: buildCountryQueryHint(rawCountryQuery, resolvedCountry)
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
700
849
|
return buildToolErrorResult(name, apiResponse);
|
|
701
850
|
}
|
|
702
851
|
|
|
703
|
-
|
|
852
|
+
let payload = apiResponse.parsedBody;
|
|
853
|
+
let meta = null;
|
|
854
|
+
|
|
855
|
+
if (name === 'search_timezones') {
|
|
856
|
+
const enriched = enrichLocationSearchPayload(payload, args);
|
|
857
|
+
payload = enriched.payload;
|
|
858
|
+
meta = enriched.meta;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return buildToolSuccessResult(name, payload, {
|
|
704
862
|
endpoint: request.path,
|
|
705
863
|
url: apiResponse.url
|
|
864
|
+
}, {
|
|
865
|
+
meta
|
|
706
866
|
});
|
|
707
867
|
}
|
|
708
868
|
|