@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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/server.js +168 -8
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@findtime/mcp-server",
3
- "version": "3.25.1",
3
+ "version": "3.25.3",
4
+ "mcpName": "io.github.hkchao/findtime-mcp-server",
4
5
  "description": "Production-parity MCP server for the findtime.io Time API",
5
6
  "bin": {
6
7
  "findtime-mcp": "server.js"
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
- const apiResponse = await fetchJson(name, request);
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
- return buildToolSuccessResult(name, apiResponse.parsedBody, {
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