diff --git a/src/tools/abuse.ts b/src/tools/abuse.ts index aaea6ba..1d83d2e 100644 --- a/src/tools/abuse.ts +++ b/src/tools/abuse.ts @@ -19,9 +19,9 @@ export function registerAbuseTools(server: McpServer) { }, description: `Decision policy: this is a single-domain tool. Use it only when the user asks for abuse contact data only. If the same IP request also needs security, ownership/company/ASN, location/city, timezone, network, or currency data, call lookup_ip once with include and targeted fields/excludes instead of chaining tools. -Dedicated abuse lookup via GET /v3/abuse. Paid only. Cost: 1 credit. Returns route, country, organization, address, emails, and phone numbers for reporting abuse. +Dedicated abuse lookup via GET /v3/abuse. Paid only. Cost: 1 credit. Returns JSON rooted at ip and abuse with route, registered country, name, organization, kind, address, emails, and phone_numbers for reporting abuse. -Use lookup_ip with include=abuse when the same request also needs geolocation or other IP domains. Tool selection rule: if this tool is used, call it once per IP target and post-process locally. Do not re-call get_abuse_contact for the same IP just to change fields/excludes or to reformat output.`, +fields/excludes use comma-separated paths such as abuse.emails; ip is always returned. force_refresh bypasses this server's cache only when the user asks. Use lookup_ip with include=abuse when the same request also needs geolocation or other IP domains. If this tool is used, call it once per IP target and post-process locally.`, inputSchema: { ip: z .string() diff --git a/src/tools/asn.ts b/src/tools/asn.ts index f3df8e0..7f4525b 100644 --- a/src/tools/asn.ts +++ b/src/tools/asn.ts @@ -92,15 +92,11 @@ export function registerAsnTools(server: McpServer) { annotations: { readOnlyHint: true, }, - description: `Decision policy: call lookup_asn once per ASN/IP target with required include values decided up front. Hard rule: do not call lookup_asn a second time for the same target in the same answer unless the first call failed or required data is truly missing. + description: `Detailed ASN lookup via GET /v3/asn. Paid only. Cost: 1 credit per successful lookup. Query by AS number or IP. Returns JSON rooted at asn, with core ASN details and optional peers, downstreams, upstreams, routes, and raw whois_response. -If the first response already contains a superset of needed data, extract the requested subset locally and do not call lookup_asn again just to trim output. +Use lookup_ip for basic ASN fields included with IP geolocation. Use lookup_asn when the user asks for ASN relationships, route prefixes, allocation details, or WHOIS. Decide include values before the call, then use fields/excludes dot paths to trim the response locally. -Detailed ASN lookup via GET /v3/asn. Paid only. Cost: 1 credit. Query by AS number or IP. Returns core ASN details and can include peers, downstreams, upstreams, routes, and WHOIS. - -Use lookup_ip for basic ASN data. Use this tool only when you need extended ASN datasets. - -Tool selection rule: if this tool is used, call it once per ASN/IP target and include set, then post-process locally. Do not re-call lookup_asn for the same target only to change fields/excludes or output shape.`, +asn takes priority over ip. include accepts peers, downstreams, upstreams, routes, and whois_response. fields/excludes can use full paths such as asn.upstreams.as_number or root-relative paths such as upstreams.as_number. force_refresh bypasses this server's cache only when the user asks. Call this tool once per ASN/IP target and post-process locally.`, inputSchema: { asn: z .string() diff --git a/src/tools/astronomy.ts b/src/tools/astronomy.ts index 074d422..e12124e 100644 --- a/src/tools/astronomy.ts +++ b/src/tools/astronomy.ts @@ -53,7 +53,7 @@ Returns location details plus astronomy data such as sunrise, sunset, moonrise, Use this tool for one date, especially when the answer needs real-time positional fields such as sun or moon altitude and azimuth. Use get_astronomy_time_series instead for daily sunrise, sunset, moon, or twilight data across a date range. -The lang parameter for non-English location field responses is available on paid plans only. On free plans, using a non-English lang value returns 401 Unauthorized.`, +lat and long must be provided together; date must be YYYY-MM-DD; elevation must be 0-10000 meters. time_zone changes timestamp formatting to include the full date. lang only changes location fields; non-English lang is paid-only and returns 401 on free plans.`, inputSchema: { lat: z .string() @@ -153,7 +153,7 @@ The lang parameter for non-English location field responses is available on paid Returns location details plus an astronomy array with one daily entry per date. Use get_astronomy instead when you need real-time positional fields such as sun or moon altitude and azimuth. -Location can be specified by coordinates, city/address, or IP. If no location is given, uses the caller's IP.`, +Location can be specified by coordinates, city/address, or IP. If no location is given, uses the caller's IP. dateStart and dateEnd are required YYYY-MM-DD values with a maximum 90-day span. lat and long must be provided together; elevation must be 0-10000 meters. time_zone changes timestamp formatting to include the full date. lang only changes location fields; non-English lang is paid-only and returns 401 on free plans. force_refresh bypasses this server's cache only when the user asks.`, inputSchema: { lat: z .string() diff --git a/src/tools/geolocation.ts b/src/tools/geolocation.ts index 39bb06c..fd67058 100644 --- a/src/tools/geolocation.ts +++ b/src/tools/geolocation.ts @@ -214,11 +214,13 @@ export function registerGeolocationTools(server: McpServer) { annotations: { readOnlyHint: true, }, - description: `Unified IP lookup via GET /v3/ipgeo. Cost: 1 credit. Use this first when one IP request needs multiple domains such as location, company/ASN, network, timezone, currency, security, or abuse. + description: `Unified IP lookup via GET /v3/ipgeo. Cost: 1 credit for base IP data; security adds 2 credits and abuse adds 1 credit when included. Use this first when one IP request needs multiple domains such as location, company/ASN, network, timezone, currency, security, or abuse. Free plan supports core location data, country metadata, currency, time_zone, basic ASN, and fields/excludes. Paid plan adds domain lookup, company, network, extended ASN, non-English lang, and include modules such as security or abuse. If fields reference an include-only module, this server infers the required include automatically. -Omit ip to use the caller's IP. Use lookup_asn only for peers, upstreams, downstreams, routes, or WHOIS. Use check_security or get_abuse_contact only for single-domain lookups.`, +Returns JSON with root IP data plus objects such as location, country_metadata, currency, asn, network, company, and time_zone. Requested include modules add objects such as security, abuse, user_agent, hostname, dma_code, or geo_accuracy. + +Omit ip to use the caller's IP. fields/excludes use comma-separated dot paths; ip is always returned, non-existent excludes do not error, and include takes priority over fields/excludes. Use lookup_asn only for peers, upstreams, downstreams, routes, or WHOIS. Use check_security or get_abuse_contact only for single-domain lookups.`, inputSchema: { ip: z .string() @@ -303,9 +305,9 @@ Omit ip to use the caller's IP. Use lookup_asn only for peers, upstreams, downst annotations: { readOnlyHint: true, }, - description: `Bulk IP lookup via POST /v3/ipgeo-bulk. Paid only. Cost: 1 credit per IP for base geolocation. This MCP server accepts up to ${MAX_BULK_ITEMS.toLocaleString()} IPs per request. + description: `Bulk IP lookup via POST /v3/ipgeo-bulk. Paid only. Cost: 1 credit per valid IP for base geolocation. This MCP server accepts up to ${MAX_BULK_ITEMS.toLocaleString()} IPs per request. -Use it when multiple IPs need location or mixed IP domains. Include modules such as security or abuse add their normal per-IP credit costs. For bulk security-only checks, prefer bulk_security_check.`, +Use it when multiple IPs need location or mixed IP domains. Include modules such as security or abuse add their normal per-valid-IP credit costs. Private, bogon, and malformed IPs are not billed by the upstream API. fields, excludes, lang, and include behave like lookup_ip for each item. For bulk security-only checks, prefer bulk_security_check.`, inputSchema: { ips: z .array(z.string()) @@ -388,7 +390,7 @@ Use it when multiple IPs need location or mixed IP domains. Include modules such readOnlyHint: true, }, description: - "Return the public IP address of the machine running this MCP server via /v3/getip. No API key or credits required.", + "Return the public IP address of the machine running this MCP server via GET /v3/getip. No API key, account, or credits are required. Returns a plain IP address string, not geolocation data. Use this only when the user asks for the server or caller public IP; use lookup_ip for location, ASN, timezone, currency, security, or abuse data.", inputSchema: {}, }, async () => { @@ -412,9 +414,9 @@ Use it when multiple IPs need location or mixed IP domains. Include modules such }, description: `Decision policy: this is a single-domain tool. Use it only when the user asks for ownership data (company/ASN) only. If the same IP request also needs security, abuse, location/city, timezone, network, or currency data, call lookup_ip once with include and targeted fields/excludes instead of chaining tools. -Ownership lookup via GET /v3/ipgeo with company and ASN only. Paid only. Cost: 1 credit. Returns the company using the IP and the ASN holder routing it. +Ownership lookup via GET /v3/ipgeo with company and ASN only. Paid only. Cost: 1 credit. Returns JSON containing company and asn objects: company name/type/domain plus ASN as_number, organization, country, type, domain, date_allocated, and rir when available. -Tool selection rule: if this tool is used, call it once per IP target and post-process locally. Do not re-call lookup_company for the same IP just to change output shape.`, +ip is optional and omitted means the caller's IP. force_refresh bypasses this server's cache only when the user asks to refresh or rerun. If this tool is used, call it once per IP target and post-process locally.`, inputSchema: { ip: z .string() @@ -459,7 +461,9 @@ Tool selection rule: if this tool is used, call it once per IP target and post-p }, description: `Currency and country metadata lookup via GET /v3/ipgeo with currency and country_metadata only. Works on free and paid plans. Cost: 1 credit. -Tool selection rule: use this tool for currency-only or country-metadata-only requests. If the request needs more IP data, prefer one lookup_ip call with targeted fields/excludes.`, +Returns JSON containing currency and country_metadata objects, including currency code/name/symbol and country-level metadata returned by the upstream API. ip is optional and omitted means the caller's IP. force_refresh bypasses this server's cache only when the user asks. + +Use this tool for currency-only or country-metadata-only requests. If the request needs more IP data, prefer one lookup_ip call with targeted fields/excludes.`, inputSchema: { ip: z .string() @@ -502,9 +506,9 @@ Tool selection rule: use this tool for currency-only or country-metadata-only re annotations: { readOnlyHint: true, }, - description: `Network lookup via GET /v3/ipgeo with network only. Paid only. Cost: 1 credit. Returns route prefix, connection type, and anycast status. + description: `Network lookup via GET /v3/ipgeo with network only. Paid only. Cost: 1 credit. Returns JSON containing the network object, including route prefix, connection type, anycast status, and related routing fields returned by the upstream API. -Tool selection rule: use this tool for network-only requests. If the request also needs other IP domains, prefer one lookup_ip call with include plus targeted fields/excludes.`, +ip is optional and omitted means the caller's IP. force_refresh bypasses this server's cache only when the user asks. Use this tool for network-only requests. If the request also needs other IP domains, prefer one lookup_ip call with include plus targeted fields/excludes.`, inputSchema: { ip: z .string() diff --git a/src/tools/security.ts b/src/tools/security.ts index 38b1157..e559f69 100644 --- a/src/tools/security.ts +++ b/src/tools/security.ts @@ -48,9 +48,9 @@ export function registerSecurityTools(server: McpServer) { }, description: `Decision policy: this is a single-domain tool. Use it only when the user asks for security/threat data only. If the same IP request also needs ownership/company/ASN, location/city, network, timezone, currency, or abuse data, call lookup_ip once with include and targeted fields/excludes instead of chaining tools. -Dedicated IP security lookup via GET /v3/security. Paid only. Cost: 2 credits. Returns threat score plus VPN, proxy, Tor, bot, spam, attacker, relay, anonymity, and cloud-provider indicators. +Dedicated IP security lookup via GET /v3/security. Paid only. Cost: 2 credits. Returns JSON rooted at ip and security with threat score plus VPN, proxy, Tor, bot, spam, attacker, relay, anonymity, and cloud-provider indicators. -Use lookup_ip with include=security when the same request also needs other IP domains. Tool selection rule: if this tool is used, call it once per IP target and post-process locally. Do not re-call check_security for the same IP just to change fields/excludes or to reformat output.`, +fields/excludes use comma-separated dot paths such as security.threat_score; ip is always returned. force_refresh bypasses this server's cache only when the user asks. Use lookup_ip with include=security when the same request also needs other IP domains. If this tool is used, call it once per IP target and post-process locally.`, inputSchema: { ip: z .string() @@ -118,9 +118,9 @@ Use lookup_ip with include=security when the same request also needs other IP do }, description: `Decision policy: this is a single-domain bulk tool. Use it only when the user asks for security/threat data only. If each IP request also needs other domains (ownership, location, network, timezone, currency, or abuse), call bulk_lookup_ip once with include and targeted fields/excludes. -Bulk IP security lookup via POST /v3/security-bulk for up to ${MAX_BULK_ITEMS.toLocaleString()} IPs per MCP request. Paid only. Cost: 2 credits per valid IP. +Bulk IP security lookup via POST /v3/security-bulk for up to ${MAX_BULK_ITEMS.toLocaleString()} IPs per MCP request. Paid only. Cost: 2 credits per valid IP. Private, bogon, and malformed IPs are not billed by the upstream API. -Use bulk_lookup_ip with include=security when the same batch also needs geolocation or other IP domains. Tool selection rule: call this tool once per IP batch and post-process locally. Do not re-call bulk_security_check for the same batch only to change fields/excludes or output shape.`, +Returns one security result per valid IP with the same security object as check_security. fields/excludes use security.* dot paths for each item; force_refresh bypasses this server's cache only when the user asks. Use bulk_lookup_ip with include=security when the same batch also needs geolocation or other IP domains. Call this tool once per IP batch and post-process locally.`, inputSchema: { ips: z .array(z.string()) diff --git a/src/tools/timezone.ts b/src/tools/timezone.ts index 660121f..638c8d6 100644 --- a/src/tools/timezone.ts +++ b/src/tools/timezone.ts @@ -23,7 +23,7 @@ Returns location details plus a time_zone object with offsets, current time valu Use this tool when the user asks what timezone or current local time applies to one place, IP, airport, or UN/LOCODE. Use convert_timezone instead when the user provides a source and destination and asks to convert a time between them. -The lang parameter for non-English responses is available on paid plans only. On free plans, using a non-English lang value returns 401 Unauthorized.`, +Parameter priority follows the upstream API: tz, then lat/long, location, ip, iata_code, icao_code, then lo_code. lat and long must be provided together. The lang parameter only changes location fields; non-English lang is paid-only and returns 401 on free plans.`, inputSchema: { tz: z .string() @@ -114,7 +114,9 @@ The lang parameter for non-English responses is available on paid plans only. On }, description: `Convert time between two locations via GET /v3/timezone/convert. Works on free and paid plans. Cost: 1 credit. -Specify source and destination by IANA timezone, coordinates, location, airport code, or UN/LOCODE. Returns original time, converted time, diff_hour, and diff_min.`, +Use this when converting a supplied time or current time from one source to one destination. Use get_timezone instead for one place's current time or timezone metadata. + +Specify one complete source selector and one complete destination selector by IANA timezone, coordinates, location, airport code, or UN/LOCODE. Coordinate selectors require both lat and long on each side. time is optional and must be yyyy-MM-dd HH:mm or yyyy-MM-dd HH:mm:ss when supplied. Returns original time, converted time, diff_hour, and diff_min.`, inputSchema: { time: z .string() diff --git a/src/tools/useragent.ts b/src/tools/useragent.ts index 42f7720..961c673 100644 --- a/src/tools/useragent.ts +++ b/src/tools/useragent.ts @@ -24,9 +24,9 @@ export function registerUserAgentTools(server: McpServer) { annotations: { readOnlyHint: true, }, - description: `Parse an explicit user-agent string via POST /v3/user-agent. Paid only. Cost: 1 credit. Returns browser, device, OS, and engine details. + description: `Parse an explicit user-agent string via POST /v3/user-agent. Paid only for this POST-based tool. Cost: 1 credit per successful user-agent. Returns JSON with parsed user_agent_string, name, version, version_major, device, engine, and operating_system fields; device and OS types can classify Robot, Hacker, Anonymized, or Unknown values. -This MCP tool parses only the uaString you pass. It does not infer a caller user-agent from the MCP transport. For multiple strings, use bulk_parse_user_agent.`, +uaString must be the exact non-empty user-agent string to parse. This MCP tool does not infer a caller user-agent from the MCP transport. force_refresh bypasses this server's cache only when the user asks. Use bulk_parse_user_agent for multiple strings.`, inputSchema: { uaString: z .string() @@ -69,9 +69,9 @@ This MCP tool parses only the uaString you pass. It does not infer a caller user annotations: { readOnlyHint: true, }, - description: `Bulk user-agent parsing via POST /v3/user-agent-bulk for up to ${MAX_BULK_ITEMS.toLocaleString()} strings per MCP request. Paid only. Cost: 1 credit per string. + description: `Bulk user-agent parsing via POST /v3/user-agent-bulk for up to ${MAX_BULK_ITEMS.toLocaleString()} strings per MCP request. Paid only. Cost: 1 credit per successful user-agent string. -Use this tool for multiple user-agent strings. For a single string, use parse_user_agent.`, +Returns one parsed result per string with the same user_agent_string, name, version, version_major, device, engine, and operating_system fields as parse_user_agent. uaStrings must be a non-empty array of explicit user-agent strings. force_refresh bypasses this server's cache only when the user asks. Use parse_user_agent for one string.`, inputSchema: { uaStrings: z .array(z.string()) diff --git a/tests/mcp.protocol.test.mjs b/tests/mcp.protocol.test.mjs index dae8552..96063f4 100644 --- a/tests/mcp.protocol.test.mjs +++ b/tests/mcp.protocol.test.mjs @@ -167,7 +167,7 @@ test("timezone and astronomy tool docs include Glama-facing selection guidance", assert.match( timezoneSource, - /Free plan returns 401 for non-English language values\./ + /non-English lang is paid-only and returns 401 on free plans\./ ); assert.match( timezoneSource, @@ -176,7 +176,7 @@ test("timezone and astronomy tool docs include Glama-facing selection guidance", assert.match(timezoneSource, /Use convert_timezone instead/i); assert.match( astronomySource, - /Free plan returns 401 for non-English language values\./ + /non-English lang is paid-only and returns 401 on free plans\./ ); assert.match( astronomySource, @@ -185,6 +185,26 @@ test("timezone and astronomy tool docs include Glama-facing selection guidance", assert.match(astronomySource, /Use get_astronomy_time_series instead/i); }); +test("tool descriptions include response shape and parameter semantics for Glama scoring", async () => { + const geolocationSource = await readRepoFile("src/tools/geolocation.ts"); + const securitySource = await readRepoFile("src/tools/security.ts"); + const asnSource = await readRepoFile("src/tools/asn.ts"); + const abuseSource = await readRepoFile("src/tools/abuse.ts"); + const userAgentSource = await readRepoFile("src/tools/useragent.ts"); + + assert.match(geolocationSource, /Returns JSON with root IP data/); + assert.match(geolocationSource, /fields\/excludes use comma-separated dot paths/); + assert.match(geolocationSource, /not geolocation data/); + assert.match(geolocationSource, /Private, bogon, and malformed/); + assert.match(securitySource, /Returns JSON rooted at ip and security/); + assert.match(securitySource, /security\.\* dot paths/); + assert.match(asnSource, /Returns JSON rooted at asn/); + assert.match(asnSource, /asn takes priority over ip/); + assert.match(abuseSource, /Returns JSON rooted at ip and abuse/); + assert.match(userAgentSource, /Paid only for this POST-based tool/); + assert.match(userAgentSource, /uaString must be the exact non-empty user-agent string/); +}); + test("README keeps parse_user_agent as paid-only without mentioning GET in that section", async () => { const readme = await readRepoFile("README.md"); const match = readme.match( diff --git a/tests/tools.integration.test.mjs b/tests/tools.integration.test.mjs index 81d89e8..436afea 100644 --- a/tests/tools.integration.test.mjs +++ b/tests/tools.integration.test.mjs @@ -362,7 +362,7 @@ test("registers all MCP tools with expected metadata", async (t) => { ); assert.match( tools.get("lookup_asn").definition.description, - /call lookup_asn once per ASN\/IP target/i + /Call this tool once per ASN\/IP target/i ); const bulkLookupIpsSchema = tools.get("bulk_lookup_ip").definition.inputSchema.ips;