Koskamo

The Tennis Desk MCP

A Model Context Protocol server that allows AI assistants to query recorded USTA NorCal adult league tennis results. It computes player Desk levels, answers comparative questions about players, teams, rosters, and opponents, and cites specific matches behind its answers. It supports both anonymous queries and authenticated sessions for forecasts using OAuth 2.1.

Endpoint and access

No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.

The Tennis Desk MCP logo, by The Tennis DeskCommunity listingStreamable HTTPNo sign-inEndpoint checked 2026-11-01Published by The Tennis Desk
MCP endpoint (Streamable HTTP)
https://thetennisdesk.com/api/mcp
Vendor documentation

What you can do with it

  • Query player Desk levels and match records
  • Get lineup win chances and ratings
  • Find teams, leagues, and clubs by name
  • Answer comparative questions about opponents

Source and maintenance

Community listingRegistry 0.2.0

Protocol and capabilities

Server
thetennisdesk 0.3.0
MCP protocol
2025-06-18
Tools
29 advertised
Resources
Not advertised
Resource templates
Not advertised
Prompts
Not advertised
Server-advertised capabilities
{
  "tools": {
    "listChanged": false
  }
}

Server-provided instructions

Untrusted server output shown for inspection. It is not Koskamo guidance and is excluded from knowledge search.

For a question about The Tennis Desk itself, call read_docs, which reads the Desk's own docs. The Desk holds USTA adult league rosters, scorecards and match lines for covered clubs, and a Desk level for players in them. Pass on each lookup's `scope`. Put the person's question, verbatim, in any `asked` parameter. When you name a team, player, club or league, link it to its address in `pages`. Connect to https://thetennisdesk.com/api/mcp.

Tools

29 advertised

plan_lineup — Your lineup and line chances
Advertised by the server during the last successful anonymous check.
Advertised read-only

Plans a doubles and singles lineup for a team the Desk covers. It lists who has played most for your team this season, with their appearances, usual line and Desk level, the Desk's estimate of how they play now. Against a covered opponent it adds each line's win chance, each team's Desk level there and the expected number of lines won. Call it for 'what lineup should we play against X', 'who has played most for us' or 'how do we beat X'. Pass `unavailable` for your players who are out, but pass `lineup` only for the players the person put on each line. Needs a Desk Pass after the member's free forecast each week. Past that, a call gets the free part, with no chance for anything still to be played, and `deskPass`, whose `message` you give the person. Its error is free_forecast_used, or desk_pass_required for a key with no member.

plan_lineup input schema
{
  "type": "object",
  "required": [
    "team"
  ],
  "properties": {
    "team": {
      "type": "string",
      "description": "Your team, e.g. 'Belvedere 18AM3.5A', or its Desk page link or number. Must be a covered team."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "lineup": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "line",
          "players"
        ],
        "properties": {
          "line": {
            "type": "string",
            "description": "The line, as 'Singles 1' or 'Doubles 2'."
          },
          "players": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Your players on that line: one for singles, two for doubles. Names may be abbreviated ('Jane')."
          }
        }
      },
      "description": "Your own team's lineup, only as the person states it, line by line. Never fill it in yourself. Each line named is read from those players."
    },
    "season": {
      "type": "integer",
      "description": "Season year. Defaults to the current season."
    },
    "opponent": {
      "type": "string",
      "description": "The opponent team to plan against, by name or Desk page link. Optional."
    },
    "unavailable": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Players on YOUR team who are out. They leave your list and the averages your side of each line is read from. Names may be abbreviated ('Jane')."
    }
  }
}
plan_lineup annotations
{
  "title": "Your lineup and line chances",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_team_record — A team's results and next matches
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns the recorded lines of any team the Desk covers, in any league and season, each with an evidenceId you can cite. It also gives the team's record, which is already counted, every team match of the period and its next matches. Call it when someone asks about a named team's matches, results or next match, or before you state a fact about them.

query_team_record input schema
{
  "type": "object",
  "required": [
    "team"
  ],
  "properties": {
    "date": {
      "type": "string",
      "description": "Optional match day, as YYYY-MM-DD, e.g. '2026-02-05'. The rows are then that day's lines, from any season unless `season` is given. Pass it when the question names the day of a match."
    },
    "team": {
      "type": "string",
      "description": "Team name or partial name to resolve, e.g. 'Tiburon' or 'Mill Valley men's 18'. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "limit": {
      "type": "integer",
      "maximum": 60,
      "minimum": 1,
      "description": "Max rows to return (default 9, max 60)."
    },
    "phase": {
      "type": "string",
      "description": "Optional competition phase: 'sectionals', 'playoffs', 'postseason', or 'regular season'. Pass it when the member names one — a sectional match keeps its REGULAR-SEASON league name in our data, so there is no other way to find it. Omit to return every phase."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year to filter to, e.g. 2026"
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY when the user explicitly asks about prior years / all history."
    }
  }
}
query_team_record annotations
{
  "title": "A team's results and next matches",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_matchup — Recent results between two teams
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns the recorded lines from the scorecards where two named teams played each other, in any season, each with an evidenceId. Call it for the head-to-head between two teams, or before you state a fact about it.

query_matchup input schema
{
  "type": "object",
  "required": [
    "team_a",
    "team_b"
  ],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "limit": {
      "type": "integer",
      "maximum": 60,
      "minimum": 1,
      "description": "Max rows to return (default 9, max 60)."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year to filter to, e.g. 2026"
    },
    "team_a": {
      "type": "string",
      "description": "First team name or partial name to resolve. Or the team's Desk page link or number."
    },
    "team_b": {
      "type": "string",
      "description": "Second team name or partial name to resolve. Or the team's Desk page link or number."
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY when the user explicitly asks about prior years / all history."
    }
  }
}
query_matchup annotations
{
  "title": "Recent results between two teams",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_roster — A team's roster and ratings
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns a team's roster for a season, one row per player with an evidenceId: name, NTRP rating, wins, losses, matches and Desk level. Call it when someone asks who is on a team, or before you state a fact about a roster. Pass `season` when the person named a year, because without it the answer covers the current season. If `moreRows` is true, call again with a larger `limit`, up to 60.

query_roster input schema
{
  "type": "object",
  "required": [
    "team"
  ],
  "properties": {
    "team": {
      "type": "string",
      "description": "Team name or partial name to resolve. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "limit": {
      "type": "integer",
      "maximum": 60,
      "minimum": 1,
      "description": "Max roster rows to return (default 30 = the full roster, max 60). Omit to get the whole team."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year to filter to, e.g. 2026"
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY when the user explicitly asks about prior years / all history."
    }
  }
}
query_roster annotations
{
  "title": "A team's roster and ratings",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_player_history — A player's recent results
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns the recorded lines a named player appeared in, as cited rows told from the player's side, with the player's record, which is already counted by season, team and stage. Call it when someone asks about a player's results, form or history. Never guess at their injury or availability, because the Desk does not hold it.

query_player_history input schema
{
  "type": "object",
  "required": [
    "player"
  ],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "limit": {
      "type": "integer",
      "maximum": 60,
      "minimum": 1,
      "description": "Max rows to return (default 9, max 60)."
    },
    "player": {
      "type": "string",
      "description": "Player name to resolve, e.g. 'First Last'. Or the player's Desk page link or number."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year to filter to, e.g. 2026"
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY when the user explicitly asks about prior years / all history."
    }
  }
}
query_player_history annotations
{
  "title": "A player's recent results",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_match_log — Desk level, match by match
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns a player's match-by-match rating history for one season: each match they played, their level before and after it, and the other players on court with their levels then. Call it when someone asks how a player's level changed, how they reached it, what one result did to it, or asks to see their matches. Prefer it to query_player_history when the question is about the level rather than the results, because it follows the level match by match.

query_match_log input schema
{
  "type": "object",
  "required": [
    "player"
  ],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "player": {
      "type": "string",
      "description": "Player name as the member wrote it. Or the player's Desk page link or number."
    },
    "season": {
      "type": "integer",
      "description": "Season year. Omit for the current season."
    }
  }
}
query_match_log annotations
{
  "title": "Desk level, match by match",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
compute_aggregate — A team's win rate and counts
Advertised by the server during the last successful anonymous check.
Advertised read-only

Works out one number for any covered team, in any league or season, in code from its recorded lines: `win_rate`, `win_count`, `loss_count` or `match_count`. Call it whenever you would state a count, an average or a win rate, and quote its value exactly rather than working one out. With no `phase` it counts the playoffs and sectionals too, so pass phase:'regular season' for a league record alone.

compute_aggregate input schema
{
  "type": "object",
  "required": [
    "team",
    "kind"
  ],
  "properties": {
    "kind": {
      "enum": [
        "win_rate",
        "win_count",
        "loss_count",
        "match_count"
      ],
      "type": "string",
      "description": "Which number to compute, over the lines the team played: 'win_rate' (the share of them won, from 0 to 1), 'win_count' (lines won), 'loss_count' (lines lost) or 'match_count' (lines played)."
    },
    "team": {
      "type": "string",
      "description": "Team key or display name. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "phase": {
      "type": "string",
      "description": "Optional competition phase: 'sectionals', 'playoffs', 'postseason', or 'regular season'. Pass it when the member names one, or when they ask for a LEAGUE record — a sectional match keeps its REGULAR-SEASON league name in our data, so there is no other way to separate them. Omit to count every phase."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year to filter to, e.g. 2026"
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY when the user explicitly asks about prior years / all history."
    }
  }
}
compute_aggregate annotations
{
  "title": "A team's win rate and counts",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
compute_roster_strength — Each rating group's record
Advertised by the server during the last successful anonymous check.
Advertised read-only

Splits a team's roster by published NTRP level and gives each level group's players, wins, losses and win rate this season, over every league line those players appeared in. Call it for a roster's shape rather than its results, as in 'how strong is their 4.0 half' or 'who carries this team'. It works even before a season has been played, because it then reads the season before.

compute_roster_strength input schema
{
  "type": "object",
  "required": [
    "team"
  ],
  "properties": {
    "team": {
      "type": "string",
      "description": "Team key or display name. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "phase": {
      "type": "string",
      "description": "Optional competition phase: 'sectionals', 'playoffs', 'postseason', or 'regular season'. Pass it when the member names one, or when they ask how a level group did in their LEAGUE — a sectional match keeps its REGULAR-SEASON league name in our data, so there is no other way to separate them. Omit to count every phase."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year, e.g. 2026"
    }
  }
}
compute_roster_strength annotations
{
  "title": "Each rating group's record",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
compute_match_line_ratings — Matches ranked by lineup strength
Advertised by the server during the last successful anonymous check.
Advertised read-only

Ranks a team's recorded matches by how strong a lineup it fielded, from each player's current Desk level, so it is not a rating at the time of the match. Call it when someone asks which match had the strongest or weakest lineup, or how a team's line strength moved.

compute_match_line_ratings input schema
{
  "type": "object",
  "required": [
    "team"
  ],
  "properties": {
    "team": {
      "type": "string",
      "description": "Team key or display name to resolve, e.g. 'Belvedere mixed 8.0'. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "season": {
      "type": "integer",
      "description": "Optional season year to filter to, e.g. 2026"
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY when the user explicitly asks about prior years / all history."
    }
  }
}
compute_match_line_ratings annotations
{
  "title": "Matches ranked by lineup strength",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_player_rating — A player's Desk level and record
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns the Desk's read of a named player: their Desk level, their record by season, team, stage and format, the model card id and the lines behind it. Call it when someone asks how strong a player is, or about their rating, level, trend or bump risk. The USTA band is not here, because it is the `rating` on a query_roster row. The Desk publishes no bump prediction.

query_player_rating input schema
{
  "type": "object",
  "required": [
    "player"
  ],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "player": {
      "type": "string",
      "description": "Player name to resolve, e.g. 'First Last'. Or the player's Desk page link or number."
    }
  }
}
query_player_rating annotations
{
  "title": "A player's Desk level and record",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
predict_matchup — One line's win chance
Advertised by the server during the last successful anonymous check.
Advertised read-only

Gives the home side's win probability for one line between named players. It weighs each player's Desk level, how a pair plays together and who is available, while a player the Desk has not yet rated gets a default level. Call it when someone asks who would win, the odds of a matchup, or how a pairing stacks up. Needs a Desk Pass after the member's free forecast each week. Past that, a call gets the free part, with no chance for anything still to be played, and `deskPass`, whose `message` you give the person. Its error is free_forecast_used, or desk_pass_required for a key with no member.

predict_matchup input schema
{
  "type": "object",
  "required": [
    "home_players",
    "away_players"
  ],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "line_type": {
      "enum": [
        "singles",
        "doubles"
      ],
      "type": "string",
      "description": "Optional; inferred from side sizes when omitted. One of 'singles' or 'doubles'."
    },
    "away_players": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 2,
      "minItems": 1,
      "description": "Away-side player name(s), ideally as they appear on recorded lines. Each may also be a player's Desk page link or number."
    },
    "home_players": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 2,
      "minItems": 1,
      "description": "Home-side player name(s), ideally as they appear on recorded lines, e.g. 'Last, First'. Each may also be a player's Desk page link or number."
    }
  }
}
predict_matchup annotations
{
  "title": "One line's win chance",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_team_forecast — Upcoming matches
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns a team's upcoming matches on the league schedule, or those of every team a player is on: the opponent, the day, the start time, and home or away. Call it for a next match, today's or tomorrow's match, or the schedule. Pass `team` for a named team, or `player` for a first-person ask like 'who am I playing'. It gives no team win chance. A team match's chance is only in the live playoff report from project_standings, which comes with the Desk Pass.

query_team_forecast input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "team": {
      "type": "string",
      "description": "Team name to resolve, e.g. 'Belvedere mixed 8.0' or 'BELVEDERE TC 40MX8.0A'. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "player": {
      "type": "string",
      "description": "Player whose teams' upcoming matches to fetch, e.g. 'First Last'. Or the player's Desk page link or number."
    }
  }
}
query_team_forecast annotations
{
  "title": "Upcoming matches",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_club_teams — A club's teams this year
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns the teams a named club fields this year, and which of them plays next. Call it whenever someone asks about a whole club by name, before you narrow the question to a single team. For how the club is doing, call query_club, which reads the club's page. Pass any league words the person used in `qualifier`, such as 'combo', 'mixed', 'womens', '40 & over' or '3.5', because dropping them answers a different question.

query_club_teams input schema
{
  "type": "object",
  "required": [
    "club"
  ],
  "properties": {
    "club": {
      "type": "string",
      "description": "The club name, e.g. 'Belvedere'."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "qualifier": {
      "type": "string",
      "description": "Any league words the asker used alongside the club — 'combo', 'mixed', 'womens', '40 and over', '3.5'. Omit only when they named the club alone."
    }
  }
}
query_club_teams annotations
{
  "title": "A club's teams this year",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_club — What a club's page shows
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns what a club's page on The Tennis Desk shows, in one call. That is its teams in a season by league, each with its place in the Desk's unofficial league table and its record. It also gives the club's next matches, latest results and record against each other club. Call it when someone asks how a club is doing, its record against other clubs, or where its teams stand. For only its list of teams, or which plays next, query_club_teams is enough. Pass `club`: the club's name as people say it or as USTA prints it, or its page address or number. Pass `season`, a year, for that season's page, and `briefing` true for the club's week. A team's `firstPlace` says whether it can still finish first, which assumes each scheduled match is played. In a result, * marks a line won or lost by default.

query_club input schema
{
  "type": "object",
  "required": [
    "club"
  ],
  "properties": {
    "club": {
      "type": "string",
      "description": "The club: its name as people say it or as USTA prints it, e.g. 'Scott Valley', or its club page address or number, e.g. '/c/218'."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "season": {
      "type": "integer",
      "minimum": 2022,
      "description": "A season year, 2022 or later: the club's page in that season, its teams, places and records then. Leave it out for the newest season the club fielded a team. A season the club has no team in is not found, and the answer names the seasons it has."
    },
    "briefing": {
      "type": "boolean",
      "description": "True for the club's week instead of its page: every team match in the next 7 days, counting the day of the call in Pacific time, each with the other team's place in the Desk's unofficial league table and their earlier meetings this season, and the checks the Desk writes from the records, each with the pages it rests on. Its `firstPlace` says whether each club team that plays in those days can still finish first, and when none plays, each team with a team match left. The answer's `briefing` is null when no team match falls in those days. Leave it out, or call again without it, for the club's page."
    }
  }
}
query_club annotations
{
  "title": "What a club's page shows",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_my_teams — Your own teams
Advertised by the server during the last successful anonymous check.
Advertised read-only

Returns the member's own teams, the ones they are on or have played for, which come from their affiliations. Call it when the person asks about their own teams, as in 'what teams am I on', and set thisYear:true for this year's. A question that names a team, a club or a player is about that one, so use the other tools for it, signed in or not. Needs a signed-in member, because it answers about them. A key with no member gets the error member_required.

query_my_teams input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "thisYear": {
      "type": "boolean",
      "description": "Set true for 'this year / this season' — the current league year's teams only. Omit for all covered teams."
    }
  }
}
query_my_teams annotations
{
  "title": "Your own teams",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_cohort_standing — Where you rank by Desk level
Advertised by the server during the last successful anonymous check.
Advertised read-only

Ranks a group of players by Desk level and gives the member's place in it. Call it for 'where do I stand', 'how do I compare to the top players', a ranking or a percentile, or the top players in an age group or level. Define the group with `ageBand`, `division` and `level`, or leave them out so it fills the group from the member's team. Needs a signed-in member, because it answers about them. A key with no member gets the error member_required.

query_cohort_standing input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "topN": {
      "type": "integer",
      "description": "How many top players to show (default 10)."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "level": {
      "type": "string",
      "description": "NTRP level as a string, e.g. '4.0' or '8.0' (mixed). Omit for a whole age-group ladder."
    },
    "season": {
      "type": "integer",
      "description": "Optional league year for cohort membership, e.g. 2026. Defaults to the current season."
    },
    "ageBand": {
      "type": "string",
      "description": "Age group: '18', '40', '55', '65', or '70' (the '& Over' band)."
    },
    "division": {
      "type": "string",
      "description": "'men', 'women', or 'mixed' — the gender ladder. Omit to use the member's own."
    },
    "includeAllSeasons": {
      "type": "boolean",
      "description": "Set true ONLY to pool cohort membership across all years."
    }
  }
}
query_cohort_standing annotations
{
  "title": "Where you rank by Desk level",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_league_standings — An unofficial league table
Advertised by the server during the last successful anonymous check.
Advertised read-only

Builds the Desk's unofficial league table for a team's flight, with the named team's place, which it ranks from recorded results by the league's own rule and tie-breaks. Call it for the standings, how a team finished, who won the league or 'the table'. Pass the team name, or nothing for the member's own team. For a whole league, pass `league` and `area` instead, such as "55 & over men's 8.0" and 'Marin'. Never pass a club or an area as the team.

query_league_standings input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "area": {
      "type": "string",
      "description": "The area of that league, as the Desk names it, e.g. 'Marin' or 'San Francisco'. Used only with `league`."
    },
    "team": {
      "type": "string",
      "description": "Team or club name whose league table to build, e.g. 'Belvedere' or 'Belvedere mixed 8.0'. Omit to use the signed-in member's own team, or to ask by `league`. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "league": {
      "type": "string",
      "description": "A league to build the table of when no team is named, with every league word the question gives, the age group included: its name, e.g. 'Adult 55 & Over Mens 8.0', or its words, e.g. \"55 & over men's 8.0\" or \"women's combo 6.5 daytime\". Used only without `team`."
    },
    "season": {
      "type": "integer",
      "description": "League year, e.g. 2025. Defaults to the current season."
    }
  }
}
query_league_standings annotations
{
  "title": "An unofficial league table",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
plan_lineup_from_availability — A lineup from NorCal availability
Advertised by the server during the last successful anonymous check.
Advertised read-only

Plans a lineup from the availability a member's agent read on the USTA NorCal league site, so the agent can fill in the lineup form. Pass the team, the match and each player's answer in USTA's own words: yes, No, maybe, call last, First shift only or Second shift only. The result also holds the values for the lineup form, and what to confirm with the member before Save. Each line's win chance needs a Desk Pass after the member's free forecast each week. The Desk never acts on the USTA site, so the member, or their own agent in their own session, presses Save. Call it when someone pastes or describes who can play a match and asks for a lineup.

plan_lineup_from_availability input schema
{
  "type": "object",
  "required": [
    "team",
    "availability"
  ],
  "properties": {
    "date": {
      "type": "string",
      "description": "The match date, as 2026-10-24. With opponent, or alone. With neither, the plan is for the team's next match."
    },
    "team": {
      "type": "string",
      "description": "The member's team: the Desk's team id, which is USTA NorCal's own team id, a link to its team page, or its name."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "season": {
      "type": "integer",
      "description": "Season year. Defaults to the current season."
    },
    "matchId": {
      "type": "string",
      "description": "USTA NorCal's own id for the match, read from the link on the team page. Optional: it fills in the form's path."
    },
    "opponent": {
      "type": "string",
      "description": "The other team in the match, as the schedule names it. With date, or alone."
    },
    "availability": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "answer": {
            "type": "string",
            "description": "USTA's answer: yes, No, maybe, call last, First shift only or Second shift only."
          },
          "player": {
            "type": "string",
            "description": "The player's name as NorCal shows it."
          },
          "ustaPlayerId": {
            "type": "string",
            "description": "The player's USTA player id, when the agent read it. It is matched before the name."
          }
        }
      },
      "description": "Each player's answer, as the agent read it on NorCal's availability grid or as the member pasted it."
    }
  }
}
plan_lineup_from_availability annotations
{
  "title": "A lineup from NorCal availability",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
prepare_score_entry — Values for the score form
Advertised by the server during the last successful anonymous check.
Advertised read-only

Prepares the values for the score form on the USTA NorCal league site, from the scores a member reports. Pass the team, the match and each line: its players on both sides, its set scores, and its status when a line was not completed. Give the sets from the member's own side, unless scoresFrom says otherwise. Pass posted too, with each line as NorCal shows it, so it can list every line where the two differ. The Desk never acts on the USTA site, so the member, or their own agent in their own session, presses Submit. Call it when someone wants to enter or check a match score on NorCal.

prepare_score_entry input schema
{
  "type": "object",
  "required": [
    "team",
    "lines"
  ],
  "properties": {
    "date": {
      "type": "string",
      "description": "The date the match was played, as 2026-10-11. The form takes the date played, even when the schedule shows another."
    },
    "team": {
      "type": "string",
      "description": "The member's team: the Desk's team id, which is USTA NorCal's own team id, a link to its team page, or its name."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "lines": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "line": {
            "type": "string",
            "description": "The line, as Singles 1 or Doubles 2."
          },
          "sets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The set scores in order, as 6-3."
          },
          "status": {
            "type": "string",
            "description": "Completed, Default, Double Default or Retired. Completed when left out."
          },
          "winner": {
            "type": "string",
            "description": "Which team won the line, us or them. Needed with scoresFrom winner."
          },
          "retired": {
            "type": "string",
            "description": "For a retired line, which team retired: us or them."
          },
          "defaulted": {
            "type": "string",
            "description": "For a defaulted line, which team defaulted: us or them."
          },
          "ourPlayers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The member's team's players on the line."
          },
          "scoresFrom": {
            "type": "string",
            "description": "Whose side the sets are written from: us, them or winner. Us when left out."
          },
          "theirPlayers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The other team's players on the line."
          }
        }
      },
      "description": "Each line as the member reports it."
    },
    "posted": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "line": {
            "type": "string",
            "description": "The line, as Singles 1 or Doubles 2."
          },
          "sets": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The set scores as NorCal shows them, from the winning team's side."
          },
          "status": {
            "type": "string",
            "description": "The line's status as NorCal shows it."
          },
          "winner": {
            "type": "string",
            "description": "Home, Visitor or Double Default."
          },
          "homePlayers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The home team's players on the line."
          },
          "visitorPlayers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The visiting team's players on the line."
          }
        }
      },
      "description": "Each line as NorCal shows it after the other team entered the score, to compare with the member's record."
    },
    "season": {
      "type": "integer",
      "description": "Season year. Defaults to the current season."
    },
    "matchId": {
      "type": "string",
      "description": "USTA NorCal's own id for the match, read from the link on the team page. Optional: it fills in the form's path."
    },
    "opponent": {
      "type": "string",
      "description": "The other team in the match, as the schedule names it. With date, or alone."
    },
    "weAreHome": {
      "type": "boolean",
      "description": "Whether the member's team was the home team. Read from the schedule when left out."
    }
  }
}
prepare_score_entry annotations
{
  "title": "Values for the score form",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
match_date_check — A home match date, checked
Advertised by the server during the last successful anonymous check.
Advertised read-only

Checks a proposed date and time for a home match before the captain posts it on the USTA NorCal league site. Pass the team, the match and the proposed date and start time, and the format and second-shift time if the captain has them. It says whether the form offers that time, which formats the form offers for the team's lines, and whether the team already plays that day. The Desk never acts on the USTA site, so the captain, or their own agent in their own session, presses Submit. Call it when a captain asks whether a date works, or before an agent fills the post-dates form.

match_date_check input schema
{
  "type": "object",
  "required": [
    "team",
    "proposedDate",
    "proposedTime"
  ],
  "properties": {
    "date": {
      "type": "string",
      "description": "The date the schedule shows for the match now, as 2026-10-24, to find it. Optional with opponent."
    },
    "team": {
      "type": "string",
      "description": "The member's team: the Desk's team id, which is USTA NorCal's own team id, a link to its team page, or its name."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "format": {
      "type": "string",
      "description": "The match format, as the form names it: All 3, 2/1 or 1/2 for three lines, and All 5, 3/2, 2/3, 4/1 or 1/4 for five."
    },
    "season": {
      "type": "integer",
      "description": "Season year. Defaults to the current season."
    },
    "opponent": {
      "type": "string",
      "description": "The other team in the match, as the schedule names it. With date, or alone."
    },
    "proposedDate": {
      "type": "string",
      "description": "The date the captain wants to post, as 2026-10-24."
    },
    "proposedTime": {
      "type": "string",
      "description": "The start time the captain wants, as 6:30 PM."
    },
    "secondShiftTime": {
      "type": "string",
      "description": "The second shift's start time, for a two-shift format."
    }
  }
}
match_date_check annotations
{
  "title": "A home match date, checked",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
availability_loop — Chase who hasn't answered
Advertised by the server during the last successful anonymous check.
Advertised read-only

Finds the players who have not answered the next match on the USTA NorCal availability grid. Pass the team and the grid as your agent read it: each match column's header, and each player's row with one cell per column. It lists each player whose answer is blank or maybe, and writes one group message for the team with the deadline and the team grid link. Each line's win chance needs a Desk Pass after the member's free forecast each week. The Desk writes, and the captain sends every message from their own phone or email. Never send a text or an email yourself, and never read a teammate's phone or email. Call it when a captain asks who has not answered on NorCal, or wants reminder texts.

availability_loop input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "rows": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "cells": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "One cell for each column, in order: the answer, won or lost once played, or empty when blank."
          },
          "player": {
            "type": "string",
            "description": "The player's name as the grid prints it, for example Able, Ann."
          },
          "ustaPlayerId": {
            "type": "string",
            "description": "The player's USTA player id, from the link on their name. It is matched before the name, and it tells two players with one name apart."
          }
        }
      },
      "description": "One row for each player on the grid."
    },
    "team": {
      "type": "string",
      "description": "The team the grid is for: the Desk's team id, which is NorCal's own team id, a link to its team page, or its name. Left out, the signed-in member's own team this season."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "format": {
      "type": "string",
      "description": "The match format, as the NorCal post-dates form names it: All 3 or All 5 for one shift, or a split such as 2/1."
    },
    "season": {
      "type": "integer",
      "description": "Season year. Defaults to the current season."
    },
    "signAs": {
      "type": "string",
      "description": "The captain's first name, which signs the lineup email."
    },
    "columns": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Each match column's header on the grid, as NorCal prints it, in order, for example #8 Sunday 10/18/26."
    },
    "matchId": {
      "type": "string",
      "description": "NorCal's own id for the match, from the Line-up link. Optional: it fills in the lineup form's path."
    },
    "deadline": {
      "type": "string",
      "description": "The answer deadline, as 2026-10-16 1:00 PM. Left out, 48 hours before the match starts."
    },
    "lineupNow": {
      "type": "boolean",
      "description": "Set true to plan the lineup now, before the deadline, from the answers so far."
    }
  }
}
availability_loop annotations
{
  "title": "Chase who hasn't answered",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
read_docs — Paragraphs from the Desk's docs
Advertised by the server during the last successful anonymous check.
Advertised read-only

Reads The Tennis Desk's own docs, word for word. They cover what the Desk is, a Desk level, what is free and what the Desk Pass adds, coverage and updates, agents, privacy and how a league table is ordered. Call it when someone asks about the Desk itself rather than a team, a player or a match. Pass `about`, a few words on what to find, and `pages` for a page you know. It needs no Desk Pass, and a call does not count toward the account's daily or monthly limit.

read_docs input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "about": {
      "type": "string",
      "description": "A few words on what to find, for example 'how it compares with NTRP'. With no `pages`, the sections of any page that match it."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "pages": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 2,
      "minItems": 1,
      "description": "One or two docs pages, each named by its address after /docs/, as https://thetennisdesk.com/docs.md lists them, for example 'desk-level'. The front page is 'index'. A page read_docs does not hold is answered with the pages it holds. Optional: leave it out to find a topic on any page."
    }
  }
}
read_docs annotations
{
  "title": "Paragraphs from the Desk's docs",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
read_research — The Desk's research figures
Advertised by the server during the last successful anonymous check.
Advertised read-only

Reads the Desk's published research about players in general, word for word. Each piece comes with its question, headline, answers and figures, the group of past players it counts and the day it was measured. Call it when someone asks about players or teams in general, such as how often players move up or back down, or home advantage. Never call it for one named player, team or match, because every figure is a rate over a group. Pass `about`, a few words on the question, or `pieces` for a piece you know. It needs no Desk Pass.

read_research input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "about": {
      "type": "string",
      "description": "A few words on the question, for example 'how players do after a bump, and how many move back down'."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "pieces": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "maxItems": 2,
      "minItems": 1,
      "description": "One or two pieces, each named by its address after /research/, as https://thetennisdesk.com/research lists them. Optional."
    }
  }
}
read_research annotations
{
  "title": "The Desk's research figures",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_coverage — What the Desk covers
Advertised by the server during the last successful anonymous check.
Advertised read-only

Answers what the Desk covers: which areas, leagues, levels, clubs, teams and seasons are on record, and what kinds of question it can answer at all. Call it whenever someone asks what you can look up, what data you have, or which leagues, levels or areas are covered. It takes no arguments, because it reports the whole census rather than a slice of it.

query_coverage input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    }
  }
}
query_coverage annotations
{
  "title": "What the Desk covers",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
project_standings — How a league could finish
Advertised by the server during the last successful anonymous check.
Advertised read-only

Projects how a league table could finish, from the matches left and the Desk's line forecasts. It gives each team's likely place, its chance to finish first and, when the Desk knows the playoff cut, its chance to make the playoffs. It also says what the named team needs, while a Desk Pass adds the team's live playoff report. Call it for a team's playoff chances or what it needs. Pass the team, or `league` and `area` for a whole league, or neither for the member's own team. Needs a Desk Pass. Without one, a call gets the free part, with no chance in it, and `deskPass`, whose `message` you give the person.

project_standings input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "area": {
      "type": "string",
      "description": "The area of that league, e.g. 'Marin'. Used only with `league`."
    },
    "team": {
      "type": "string",
      "description": "The team whose league to project, e.g. 'Belvedere men's 7.5'. Omit for the signed-in member's own team. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    },
    "league": {
      "type": "string",
      "description": "A league to project when no team is named, with its words, e.g. \"55 & over men's 8.0\". Used only without `team`."
    }
  }
}
project_standings annotations
{
  "title": "How a league could finish",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_playoff_history — Past playoff chances
Advertised by the server during the last successful anonymous check.
Advertised read-only

Gives a team's playoff chances from a week ago and earlier, one a week, each as of its day. It also says what moved the chance most: the change between two days and the matches played between them. Call it for how a team's playoff chances have changed, or what its chances were last week. Pass the team, or nothing for the member's own team. It never gives today's chance: the `project_standings` tool gives that, with the Desk Pass.

query_playoff_history input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "team": {
      "type": "string",
      "description": "The team, e.g. 'Belvedere men's 7.5'. Omit for the signed-in member's own team. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    }
  }
}
query_playoff_history annotations
{
  "title": "Past playoff chances",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
query_forecast_results — How past forecasts did
Advertised by the server during the last successful anonymous check.
Advertised read-only

Gives a team's latest matches that the Desk forecast before they were played. For each line, it gives the team's chance as the Desk logged it the night before, and whether the team won the line. It also gives each match's result. Call it for how the Desk's forecasts did for a team, or what the Desk gave a team in a past match. Pass the team, or nothing for the member's own team. It gives only matches already played, never a chance for a match still to come, which comes with the Desk Pass.

query_forecast_results input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "team": {
      "type": "string",
      "description": "The team, e.g. 'Belvedere men's 7.5'. Omit for the signed-in member's own team. Or the team's Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    }
  }
}
query_forecast_results annotations
{
  "title": "How past forecasts did",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
scout_team — A written scout of one team
Advertised by the server during the last successful anonymous check.
Advertised read-only

Writes the Desk's scout of one team it covers: its ranked reads of who matters and how the lines go, each with its evidence. Call it when someone asks for a scout, a breakdown of a team, or how to beat a named team. Pass `team`, a team the Desk covers, by name or Desk page link. Never call it for a whole club, because a scout is of one team. Needs a Desk Pass. Without one, a call gets the free part, with no chance in it, and `deskPass`, whose `message` you give the person.

scout_team input schema
{
  "type": "object",
  "required": [
    "team"
  ],
  "properties": {
    "team": {
      "type": "string",
      "description": "The team to scout, e.g. 'Belvedere 18AM3.5A', or its Desk page link or number."
    },
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    }
  }
}
scout_team annotations
{
  "title": "A written scout of one team",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}
get_desk_pass — Your Desk Pass and forecasts
Advertised by the server during the last successful anonymous check.
Advertised read-only

Says whether the signed-in member holds the Desk Pass, when it renews or ends, how many forecasts they have left this week and when more come. Call it when someone asks about their Desk Pass or their forecasts. It takes no arguments. Needs a signed-in member, because it answers about them. A key with no member gets the error member_required.

get_desk_pass input schema
{
  "type": "object",
  "required": [],
  "properties": {
    "asked": {
      "type": "string",
      "description": "The person's question word for word, never paraphrased. Leave it out rather than invent one."
    }
  }
}
get_desk_pass annotations
{
  "title": "Your Desk Pass and forecasts",
  "readOnlyHint": true,
  "openWorldHint": false,
  "idempotentHint": true,
  "destructiveHint": false
}

Prompts

Not advertised

No prompt definitions available.

Resources

Not advertised

No resource definitions available.

30-day availability

Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.

Successful checks
1 of 1 (100%)
Average latency
598 ms
95th percentile latency
598 ms

Maintenance history

Public check and source-refresh events. Editorial and ownership records remain private.

  • · probe · passed · 598 ms · metadata changed · 29 tool(s)

Connect from your client

Each snippet registers the same endpoint; pick the client you use.

This server is operated by the named third party. Listing and endpoint checks are not an endorsement or a security audit. Review the operator's permissions, terms, and data handling before connecting. How listings are checked and corrected.

Claude Code

Run this once in a terminal. It registers the remote server. Use --scope project to write .mcp.json for the repo.
Claude Code configuration
claude mcp add --transport http the-tennis-desk-mcp https://thetennisdesk.com/api/mcp

Codex

Run this once. Codex stores MCP servers in ~/.codex/config.toml (or .codex/config.toml in a trusted project).
Codex configuration
codex mcp add the-tennis-desk-mcp --url https://thetennisdesk.com/api/mcp

Cursor

Merge this server into .cursor/mcp.json in your project, then enable the-tennis-desk-mcp in your MCP settings.
Cursor configuration
{
  "mcpServers": {
    "the-tennis-desk-mcp": {
      "url": "https://thetennisdesk.com/api/mcp"
    }
  }
}

Cline

Merge this into cline_mcp_settings.json (Cline → MCP Servers → Configure).
Cline configuration
{
  "mcpServers": {
    "the-tennis-desk-mcp": {
      "type": "streamableHttp",
      "url": "https://thetennisdesk.com/api/mcp",
      "disabled": false
    }
  }
}

VS Code

Merge this server into .vscode/mcp.json. Use MCP: List Servers to start the-tennis-desk-mcp and inspect its tools.
VS Code configuration
{
  "servers": {
    "the-tennis-desk-mcp": {
      "type": "http",
      "url": "https://thetennisdesk.com/api/mcp"
    }
  }
}

Gemini CLI

Run this once. It writes .gemini/settings.json in the current project; pass --scope user for ~/.gemini/settings.json.
Gemini CLI configuration
gemini mcp add --transport http the-tennis-desk-mcp https://thetennisdesk.com/api/mcp

Claude Desktop

Merge this into claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\), then restart Claude Desktop.
Claude Desktop configuration
{
  "mcpServers": {
    "the-tennis-desk-mcp": {
      "url": "https://thetennisdesk.com/api/mcp"
    }
  }
}

Windsurf

Merge this into ~/.codeium/windsurf/mcp_config.json, then refresh MCP in Cascade. Windsurf remote servers use serverUrl, not url.
Windsurf configuration
{
  "mcpServers": {
    "the-tennis-desk-mcp": {
      "serverUrl": "https://thetennisdesk.com/api/mcp"
    }
  }
}

Let your agent find its tools

Find servers like The Tennis Desk MCP from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.