Skip to content

Commit 0146f13

Browse files
cablateclaude
andauthored
feat: tool annotations + actionable error messages + CHANGELOG (#31)
* feat: migrate to registerTool with annotations and improve error messages - Migrate from deprecated server.tool() to server.registerTool() API - Add MCP Tool Annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) to all 7 tools - Improve Google Maps API error messages with actionable guidance for HTTP 403, 429, ZERO_RESULTS, OVER_QUERY_LIMIT, REQUEST_DENIED, INVALID_REQUEST - Type-strengthen ToolConfig interface (schema: Record<string, z.ZodTypeAny>) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add CHANGELOG.md and strengthen smoke tests - Add CHANGELOG.md following Claude Code's format (version + bullet points) - Add annotations verification (readOnlyHint, destructiveHint) for all 7 tools - Add inputSchema presence check for all tools - Add E2E tests for reverse geocode, elevation, and distance matrix - Test coverage: 21 → 55 assertions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add PR/issue templates, auto-changelog, and GitHub Releases - Add pull request template with summary/changes/test plan/changelog sections - Add issue templates for bug reports and feature requests - Update release workflow to auto-generate CHANGELOG entries from commit messages - Add GitHub Release creation on publish Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64fd960 commit 0146f13

10 files changed

Lines changed: 313 additions & 18 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
name: Bug Report
3+
about: Report a bug or unexpected behavior
4+
labels: bug
5+
---
6+
7+
## Description
8+
9+
<!-- What happened? -->
10+
11+
## Steps to reproduce
12+
13+
1.
14+
2.
15+
3.
16+
17+
## Expected behavior
18+
19+
<!-- What should have happened? -->
20+
21+
## Environment
22+
23+
- Package version:
24+
- Node.js version:
25+
- MCP client (Claude Code / Cursor / other):
26+
- OS:
27+
28+
## Logs
29+
30+
<!-- Paste any relevant error messages or logs -->
31+
32+
```
33+
34+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
name: Feature Request
3+
about: Suggest a new tool or improvement
4+
labels: enhancement
5+
---
6+
7+
## Problem
8+
9+
<!-- What problem does this solve? -->
10+
11+
## Proposed solution
12+
13+
<!-- How should it work? -->
14+
15+
## Alternatives considered
16+
17+
<!-- Any other approaches you've thought about? -->

.github/pull_request_template.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Summary
2+
3+
<!-- What does this PR do? 1-3 bullet points -->
4+
5+
-
6+
7+
## Changes
8+
9+
<!-- List the key files/areas changed -->
10+
11+
| File | Change |
12+
|------|--------|
13+
| | |
14+
15+
## Test plan
16+
17+
- [ ] `npm run build` succeeds
18+
- [ ] `npm run lint` — 0 errors
19+
- [ ] `npm test` — all pass
20+
- [ ] Manual verification (if applicable)
21+
22+
## Changelog entry
23+
24+
<!-- Copy this to CHANGELOG.md under the next version -->
25+
26+
```
27+
-
28+
```

.github/workflows/release.yml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ jobs:
1313
contents: write
1414
steps:
1515
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
1618

1719
- uses: actions/setup-node@v4
1820
with:
@@ -31,6 +33,31 @@ jobs:
3133
- name: Bump version
3234
run: npm version patch --no-git-tag-version
3335

36+
- name: Update CHANGELOG
37+
run: |
38+
VERSION=$(node -p "require('./package.json').version")
39+
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
40+
41+
# Collect commit messages since last tag
42+
if [ -n "$PREV_TAG" ]; then
43+
COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s" --no-merges | grep -v "^- chore: release")
44+
else
45+
COMMITS=$(git log --pretty=format:"- %s" --no-merges -10 | grep -v "^- chore: release")
46+
fi
47+
48+
# Prepend new version to CHANGELOG.md
49+
HEADER="## ${VERSION}"
50+
TMPFILE=$(mktemp)
51+
echo "# Changelog" > "$TMPFILE"
52+
echo "" >> "$TMPFILE"
53+
echo "$HEADER" >> "$TMPFILE"
54+
echo "" >> "$TMPFILE"
55+
echo "$COMMITS" >> "$TMPFILE"
56+
echo "" >> "$TMPFILE"
57+
# Append existing content (skip the first "# Changelog" line)
58+
tail -n +2 CHANGELOG.md >> "$TMPFILE"
59+
mv "$TMPFILE" CHANGELOG.md
60+
3461
- name: Publish to npm
3562
run: npm publish --access public
3663
env:
@@ -41,8 +68,21 @@ jobs:
4168
VERSION=$(node -p "require('./package.json').version")
4269
git config user.name "github-actions[bot]"
4370
git config user.email "github-actions[bot]@users.noreply.github.com"
44-
git add package.json package-lock.json
71+
git add package.json package-lock.json CHANGELOG.md
4572
git commit -m "chore: release v${VERSION}"
4673
git tag "v${VERSION}"
4774
git push
4875
git push --tags
76+
77+
- name: Create GitHub Release
78+
run: |
79+
VERSION=$(node -p "require('./package.json').version")
80+
PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")
81+
if [ -n "$PREV_TAG" ]; then
82+
NOTES=$(git log ${PREV_TAG}..v${VERSION} --pretty=format:"- %s" --no-merges | grep -v "^- chore: release")
83+
else
84+
NOTES="Initial release"
85+
fi
86+
gh release create "v${VERSION}" --title "v${VERSION}" --notes "$NOTES"
87+
env:
88+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CHANGELOG.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Changelog
2+
3+
## 0.0.21
4+
5+
- Migrated from deprecated `server.tool()` to `server.registerTool()` API for MCP SDK v1.27+ compatibility
6+
- Added MCP Tool Annotations to all tools — clients can now auto-approve read-only, non-destructive API queries without user confirmation
7+
- Improved Google Maps API error messages with actionable guidance (e.g. HTTP 403 now suggests enabling the Places API, HTTP 429 links to quota settings)
8+
- Added CI workflow for automated build, lint, and smoke tests on pull requests
9+
- Added release workflow for automated npm publishing on merge to main
10+
- Added ESLint 9 flat config with TypeScript and Prettier integration
11+
- Added smoke test suite for annotations, multi-tool E2E, and concurrent session validation
12+
13+
## 0.0.20
14+
15+
- Upgraded `@modelcontextprotocol/sdk` from ^1.11.0 to ^1.27.1 — fixes cross-client response data leakage between concurrent sessions (GHSA-345p-7cg4-v4c7, CVSS 7.1)
16+
- Upgraded `zod` to ^3.25.0 (peer dependency of SDK v1.23+)
17+
- Fixed multi-session crash by creating per-session McpServer instances in HTTP mode
18+
- Added smoke test suite covering server init, tools/list, geocode, and concurrent sessions
19+
- Pinned `@types/express` to v4
20+
21+
## 0.0.19
22+
23+
- Updated to Google's new Places API (New) to resolve HTTP 403 errors with the legacy API
24+
25+
## 0.0.18
26+
27+
- Standardized all error messages to English with more detailed information
28+
29+
## 0.0.17
30+
31+
- Added HTTP header authentication — API keys via `X-Google-Maps-API-Key` header
32+
- Fixed concurrent user issues — each session uses its own API key
33+
- Fixed npx execution module bundling issues
34+
- Improved documentation with clearer setup instructions
35+
36+
## 0.0.14
37+
38+
- Added streamable HTTP transport support
39+
- Improved CLI interface with emoji indicators
40+
- Enhanced error handling and logging
41+
- Added comprehensive tool descriptions for LLM integration
42+
- Updated to latest MCP SDK version

src/config.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ import { ReverseGeocode, ReverseGeocodeParams } from "./tools/maps/reverseGeocod
88
import { DistanceMatrix, DistanceMatrixParams } from "./tools/maps/distanceMatrix.js";
99
import { Directions, DirectionsParams } from "./tools/maps/directions.js";
1010
import { Elevation, ElevationParams } from "./tools/maps/elevation.js";
11+
12+
// All Google Maps tools are read-only API queries
13+
const MAPS_TOOL_ANNOTATIONS = {
14+
readOnlyHint: true,
15+
destructiveHint: false,
16+
idempotentHint: true,
17+
openWorldHint: true,
18+
} as const;
19+
1120
interface ServerInstanceConfig {
12-
// Renamed from ServerConfig and modified
1321
name: string;
1422
portEnvVar: string;
1523
tools: ToolConfig[];
@@ -24,42 +32,49 @@ const serverConfigs: ServerInstanceConfig[] = [
2432
name: SearchNearby.NAME,
2533
description: SearchNearby.DESCRIPTION,
2634
schema: SearchNearby.SCHEMA,
35+
annotations: MAPS_TOOL_ANNOTATIONS,
2736
action: (params: SearchNearbyParams) => SearchNearby.ACTION(params),
2837
},
2938
{
3039
name: PlaceDetails.NAME,
3140
description: PlaceDetails.DESCRIPTION,
3241
schema: PlaceDetails.SCHEMA,
42+
annotations: MAPS_TOOL_ANNOTATIONS,
3343
action: (params: PlaceDetailsParams) => PlaceDetails.ACTION(params),
3444
},
3545
{
3646
name: Geocode.NAME,
3747
description: Geocode.DESCRIPTION,
3848
schema: Geocode.SCHEMA,
49+
annotations: MAPS_TOOL_ANNOTATIONS,
3950
action: (params: GeocodeParams) => Geocode.ACTION(params),
4051
},
4152
{
4253
name: ReverseGeocode.NAME,
4354
description: ReverseGeocode.DESCRIPTION,
4455
schema: ReverseGeocode.SCHEMA,
56+
annotations: MAPS_TOOL_ANNOTATIONS,
4557
action: (params: ReverseGeocodeParams) => ReverseGeocode.ACTION(params),
4658
},
4759
{
4860
name: DistanceMatrix.NAME,
4961
description: DistanceMatrix.DESCRIPTION,
5062
schema: DistanceMatrix.SCHEMA,
63+
annotations: MAPS_TOOL_ANNOTATIONS,
5164
action: (params: DistanceMatrixParams) => DistanceMatrix.ACTION(params),
5265
},
5366
{
5467
name: Directions.NAME,
5568
description: Directions.DESCRIPTION,
5669
schema: Directions.SCHEMA,
70+
annotations: MAPS_TOOL_ANNOTATIONS,
5771
action: (params: DirectionsParams) => Directions.ACTION(params),
5872
},
5973
{
6074
name: Elevation.NAME,
6175
description: Elevation.DESCRIPTION,
6276
schema: Elevation.SCHEMA,
77+
annotations: MAPS_TOOL_ANNOTATIONS,
6378
action: (params: ElevationParams) => Elevation.ACTION(params),
6479
},
6580
],

src/core/BaseMcpServer.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
33
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4+
import { isInitializeRequest, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
55
import express, { Request, Response } from "express";
66
import { Server } from "http";
77
import { randomUUID } from "node:crypto";
8+
import { z } from "zod";
89
import { Logger } from "../index.js";
910
import { ApiKeyManager } from "../utils/apiKeyManager.js";
1011
import { runWithContext } from "../utils/requestContext.js";
@@ -15,8 +16,9 @@ const VERSION = "0.0.1";
1516
export interface ToolConfig {
1617
name: string;
1718
description: string;
18-
schema: any; // Adjust type as per actual SDK (e.g., ZodSchema)
19-
action: (params: any) => Promise<any>; // Adjust type for params and return
19+
schema: Record<string, z.ZodTypeAny>;
20+
annotations?: ToolAnnotations;
21+
action: (params: any) => Promise<any>;
2022
}
2123

2224
export interface SessionContext {
@@ -43,7 +45,15 @@ export class BaseMcpServer {
4345
{ capabilities: { logging: {}, tools: {} } }
4446
);
4547
this.tools.forEach((tool) => {
46-
server.tool(tool.name, tool.description, tool.schema, async (params: any) => tool.action(params));
48+
server.registerTool(
49+
tool.name,
50+
{
51+
description: tool.description,
52+
inputSchema: z.object(tool.schema),
53+
annotations: tool.annotations,
54+
},
55+
async (params: any) => tool.action(params)
56+
);
4757
});
4858
return server;
4959
}

src/services/NewPlacesService.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,16 @@ export class NewPlacesService {
235235
}
236236

237237
private extractErrorMessage(error: any): string {
238-
const apiError = error?.message || error?.details || error?.status;
238+
const statusCode = error?.code;
239+
const message = error?.message || error?.details;
239240

240-
if (apiError) {
241-
return `${apiError}`;
241+
if (statusCode === 7 || statusCode === 403) {
242+
return "API key invalid or Places API (New) not enabled. Check: console.cloud.google.com → APIs & Services → Enable 'Places API (New)'";
243+
}
244+
if (statusCode === 8 || statusCode === 429) {
245+
return "API quota exceeded. Wait and retry, or check quota at console.cloud.google.com → Quotas";
242246
}
243247

244-
return error instanceof Error ? error.message : String(error);
248+
return message || (error instanceof Error ? error.message : String(error));
245249
}
246250
}

src/services/toolclass.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,39 @@ interface GeocodeResult {
3737
}
3838

3939
/**
40-
* Extracts a meaningful error message from various error types
41-
* Prioritizes Google Maps API error messages when available
40+
* Extracts a meaningful, actionable error message from Google Maps API errors.
4241
*/
4342
function extractErrorMessage(error: any): string {
44-
// Extract Google API error message if available
45-
const apiError = error?.response?.data?.error_message;
4643
const statusCode = error?.response?.status;
44+
const apiError = error?.response?.data?.error_message;
45+
const apiStatus = error?.response?.data?.status;
46+
47+
// Map common HTTP status codes to actionable messages
48+
if (statusCode === 403) {
49+
return "API key invalid or required API not enabled. Check: console.cloud.google.com → APIs & Services → Enable the relevant API (Places, Geocoding, etc.)";
50+
}
51+
if (statusCode === 429) {
52+
return "API quota exceeded. Wait and retry, or check quota at console.cloud.google.com → Quotas";
53+
}
54+
55+
// Map Google Maps API status codes
56+
if (apiStatus === "ZERO_RESULTS") {
57+
return "No results found. Try broader search terms or a larger radius.";
58+
}
59+
if (apiStatus === "OVER_QUERY_LIMIT") {
60+
return "API quota exceeded. Wait and retry, or upgrade your billing plan.";
61+
}
62+
if (apiStatus === "REQUEST_DENIED") {
63+
return `Request denied by Google Maps API. ${apiError || "Check your API key and enabled APIs."}`;
64+
}
65+
if (apiStatus === "INVALID_REQUEST") {
66+
return `Invalid request parameters. ${apiError || "Check your input values."}`;
67+
}
4768

4869
if (apiError) {
4970
return `${apiError} (HTTP ${statusCode})`;
5071
}
5172

52-
// Fallback to standard error message
5373
return error instanceof Error ? error.message : String(error);
5474
}
5575

0 commit comments

Comments
 (0)