Skip to content

Commit d67d459

Browse files
gui-wfclaude
andcommitted
Add automated API specification management
- Add fetch:swagger script with robust fallback to existing files - Add update:api workflow combining fetch + generate types + docs - Add nix flake app for manual API updates (nix run .#update-api) - Clean up flake.nix to reuse package.json abstractions - Remove network dependencies from nix build process - Update documentation with new API update workflows Addresses Issue #1 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 4c136ef commit d67d459

File tree

4 files changed

+202
-42
lines changed

4 files changed

+202
-42
lines changed

CLAUDE.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ See `KNOWN_ISSUES.md` for detailed documentation of current limitations and work
2828
```bash
2929
# Development
3030
nix develop # Enter dev environment
31+
npm run fetch:swagger # Download latest swagger spec from PurelyMail
3132
npm run generate:types # Regenerate TypeScript types from swagger
3233
npm run generate:docs # Update endpoint-descriptions.md
34+
npm run update:api # Complete API update (fetch + generate types + docs)
3335
npm run dev # Run server in development mode
3436
npm run test:mock # Test with mock data
3537
npm run inspector # Launch MCP Inspector
@@ -67,13 +69,11 @@ generated-client/ # DO NOT MODIFY - codegen output
6769

6870
## Development Workflow
6971

70-
### Adding New Endpoints
71-
1. Update `purelymail-api-spec.json` with new endpoints
72-
2. Run `npm run generate:types` to regenerate TypeScript types
73-
3. Run `npm run generate:docs` to update documentation
74-
4. Test with `MOCK_MODE=true npm run inspector`
75-
5. Implement mock responses in `mock-client.ts`
76-
6. Test with real API
72+
### Updating from PurelyMail API Changes
73+
1. Run `npm run update:api` to fetch latest spec and regenerate everything
74+
2. Test with `MOCK_MODE=true npm run inspector` to verify tool registration
75+
3. Update mock responses in `mock-client.ts` if new endpoints were added
76+
4. Test with real API to ensure compatibility
7777

7878
### Testing Changes
7979
1. Always test with mocks first: `npm run test:mock`
@@ -111,9 +111,9 @@ generated-client/ # DO NOT MODIFY - codegen output
111111
- Ensure `.js` extension in imports
112112
- Run `npm run generate:types` if types are missing
113113

114-
### Type errors after API change
115-
- Regenerate types: `npm run generate:types`
116-
- Update mock implementations to match new types
114+
### Type errors after API changes
115+
- Run `npm run update:api` to fetch latest spec and regenerate types
116+
- Update mock implementations to match new types if needed
117117

118118
### MCP Inspector can't connect
119119
- Check server is using StdioServerTransport

flake.nix

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,73 +6,109 @@
66
flake-utils.url = "github:numtide/flake-utils";
77
};
88

9-
outputs =
10-
{
11-
self,
12-
nixpkgs,
13-
flake-utils,
14-
}:
15-
flake-utils.lib.eachDefaultSystem (
16-
system:
9+
outputs = { self, nixpkgs, flake-utils }:
10+
flake-utils.lib.eachDefaultSystem (system:
1711
let
1812
pkgs = nixpkgs.legacyPackages.${system};
1913
in
2014
{
15+
# Development environment
2116
devShells.default = pkgs.mkShell {
2217
buildInputs = with pkgs; [
2318
nodejs_20
2419
nodePackages.typescript
2520
nodePackages.typescript-language-server
26-
nodePackages.pnpm
27-
# Swagger codegen tools
28-
openapi-generator-cli
29-
jq
30-
yq
21+
jq # For JSON processing
22+
yq # For YAML processing
3123
];
3224

3325
shellHook = ''
3426
echo "PurelyMail MCP Server Development Environment"
35-
echo "Node version: $(node --version)"
36-
echo "TypeScript version: $(tsc --version)"
37-
echo "OpenAPI Generator: $(openapi-generator-cli version)"
38-
39-
# Generate TypeScript client if spec exists
40-
if [ -f purelymail-api-spec.json ] && [ ! -d generated-client ]; then
41-
echo "Generating TypeScript client from swagger spec..."
42-
npm run generate:types
43-
fi
27+
echo "Node: $(node --version), TypeScript: $(tsc --version)"
28+
echo ""
29+
echo "Available commands (see package.json for details):"
30+
echo " npm run fetch:swagger - Download latest API spec"
31+
echo " npm run generate:types - Generate TypeScript types"
32+
echo " npm run update:api - Complete update workflow"
33+
echo " nix run .#update-api - Update API via nix app"
4434
'';
4535
};
4636

37+
# Production package build
4738
packages.default = pkgs.stdenv.mkDerivation {
4839
pname = "purelymail-mcp-server";
4940
version = "1.0.0";
5041
src = ./.;
5142

52-
buildInputs = with pkgs; [
53-
nodejs_20
54-
openapi-generator-cli
55-
];
43+
buildInputs = [ pkgs.nodejs_20 ];
5644

5745
buildPhase = ''
46+
# Install dependencies (production only)
5847
npm ci --production
59-
npm run generate:client
48+
49+
# Generate types if spec exists (no network calls during build)
50+
if [ -f purelymail-api-spec.json ]; then
51+
npm run generate:types
52+
else
53+
echo "Warning: purelymail-api-spec.json not found"
54+
echo "Run 'nix run .#update-api' to fetch latest spec"
55+
fi
56+
57+
# Build using package.json script
6058
npm run build
6159
'';
6260

6361
installPhase = ''
64-
mkdir -p $out/bin
65-
cp -r dist $out/
66-
cp -r generated-client $out/
67-
cp package.json $out/
62+
mkdir -p $out/{bin,share/purelymail-mcp}
6863
64+
# Copy built artifacts
65+
cp -r dist $out/share/purelymail-mcp/
66+
cp -r src/types $out/share/purelymail-mcp/
67+
cp -r src/mocks $out/share/purelymail-mcp/
68+
cp package.json $out/share/purelymail-mcp/
69+
cp purelymail-api-spec.json $out/share/purelymail-mcp/ 2>/dev/null || true
70+
71+
# Create executable
6972
cat > $out/bin/purelymail-mcp <<EOF
7073
#!/usr/bin/env node
71-
require('$out/dist/index.js')
74+
require('$out/share/purelymail-mcp/dist/index.js')
7275
EOF
7376
chmod +x $out/bin/purelymail-mcp
7477
'';
7578
};
79+
80+
# Nix app for updating API specification
81+
apps.update-api = {
82+
type = "app";
83+
program = toString (pkgs.writeScript "update-api" ''
84+
#!${pkgs.bash}/bin/bash
85+
set -euo pipefail
86+
87+
echo "🔄 Updating PurelyMail API specification..."
88+
89+
# Ensure we're in the project root
90+
if [ ! -f package.json ]; then
91+
echo "❌ Error: Must be run from the project root directory"
92+
exit 1
93+
fi
94+
95+
if [ ! -d node_modules ]; then
96+
echo "📦 Installing dependencies..."
97+
${pkgs.nodejs_20}/bin/npm install
98+
fi
99+
100+
# Use the npm script directly (reuse package.json abstraction)
101+
echo "🚀 Running update:api workflow..."
102+
${pkgs.nodejs_20}/bin/npm run update:api
103+
104+
echo "✅ API specification updated successfully!"
105+
echo ""
106+
echo "Next steps:"
107+
echo " 1. Review changes: git diff"
108+
echo " 2. Test: npm run test:mock"
109+
echo " 3. Commit if satisfied: git add . && git commit -m 'Update API specification'"
110+
'');
111+
};
76112
}
77113
);
78114
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"version": "1.0.0",
44
"type": "module",
55
"scripts": {
6+
"fetch:swagger": "node scripts/fetch-swagger.js",
67
"generate:types": "openapi-typescript purelymail-api-spec.json -o src/types/purelymail-api.ts",
78
"generate:docs": "node scripts/extract-endpoints.js > endpoint-descriptions.md",
9+
"update:api": "npm run fetch:swagger && npm run generate:types && npm run generate:docs",
810
"build": "tsc",
911
"dev": "tsx src/index.ts",
1012
"test": "vitest",

scripts/fetch-swagger.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env node
2+
import fs from 'fs';
3+
import https from 'https';
4+
import { URL } from 'url';
5+
6+
const SWAGGER_URL = 'https://news.purelymail.com/api/swagger-spec.js';
7+
const OUTPUT_JS = 'swagger-spec.js';
8+
const OUTPUT_JSON = 'purelymail-api-spec.json';
9+
10+
console.error('Fetching PurelyMail swagger specification...');
11+
12+
async function fetchSwagger() {
13+
try {
14+
const response = await fetch(SWAGGER_URL);
15+
if (!response.ok) {
16+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
17+
}
18+
19+
const jsContent = await response.text();
20+
21+
// Write the original JS file
22+
fs.writeFileSync(OUTPUT_JS, jsContent);
23+
console.error(`✓ Downloaded ${OUTPUT_JS}`);
24+
25+
// Extract the JSON spec from the JS file
26+
const spec = await extractSpecFromJS(jsContent);
27+
28+
// Write the JSON spec
29+
fs.writeFileSync(OUTPUT_JSON, JSON.stringify(spec, null, 2));
30+
console.error(`✓ Extracted ${OUTPUT_JSON}`);
31+
32+
console.error('Swagger specification updated successfully!');
33+
34+
} catch (error) {
35+
console.error(`✗ Failed to fetch from ${SWAGGER_URL}: ${error.message}`);
36+
37+
// Check if we have existing files to fall back to
38+
if (fs.existsSync(OUTPUT_JS) && fs.existsSync(OUTPUT_JSON)) {
39+
console.error(`Using existing files as fallback`);
40+
process.exit(0);
41+
} else {
42+
console.error(`No existing files found. Please check your internet connection or the URL.`);
43+
process.exit(1);
44+
}
45+
}
46+
}
47+
48+
async function extractSpecFromJS(jsContent) {
49+
try {
50+
// Try to extract the spec from the JS content
51+
// The swagger-spec.js contains: window.swaggerSpec = { ... }
52+
// We need to safely evaluate it
53+
54+
// Create a safe evaluation context with window object
55+
const windowContext = { swaggerSpec: null };
56+
57+
// Replace window.swaggerSpec with our context
58+
const safeJs = jsContent.replace(/window\.swaggerSpec\s*=/, 'windowContext.swaggerSpec =');
59+
60+
// Use Function constructor for safer evaluation than eval
61+
const fn = new Function('windowContext', safeJs + '; return windowContext.swaggerSpec;');
62+
const spec = fn(windowContext);
63+
64+
if (!spec || typeof spec !== 'object') {
65+
throw new Error('Invalid swagger spec format');
66+
}
67+
68+
// Validate it looks like a swagger spec
69+
if (!spec.openapi && !spec.swagger && !spec.info) {
70+
throw new Error('Not a valid OpenAPI/Swagger specification');
71+
}
72+
73+
return spec;
74+
75+
} catch (error) {
76+
console.error(`Failed to extract spec from JS: ${error.message}`);
77+
78+
// Fallback: try to use existing JSON file
79+
if (fs.existsSync(OUTPUT_JSON)) {
80+
console.error('Using existing JSON file as fallback');
81+
return JSON.parse(fs.readFileSync(OUTPUT_JSON, 'utf8'));
82+
}
83+
84+
throw new Error('Could not extract or find existing swagger specification');
85+
}
86+
}
87+
88+
// Polyfill for Node.js < 18
89+
if (!globalThis.fetch) {
90+
globalThis.fetch = async function(url) {
91+
return new Promise((resolve, reject) => {
92+
const parsedUrl = new URL(url);
93+
const options = {
94+
hostname: parsedUrl.hostname,
95+
port: parsedUrl.port,
96+
path: parsedUrl.pathname + parsedUrl.search,
97+
method: 'GET',
98+
headers: {
99+
'User-Agent': 'PurelyMail MCP Server'
100+
}
101+
};
102+
103+
const req = https.request(options, (res) => {
104+
let data = '';
105+
res.on('data', chunk => data += chunk);
106+
res.on('end', () => {
107+
resolve({
108+
ok: res.statusCode >= 200 && res.statusCode < 300,
109+
status: res.statusCode,
110+
statusText: res.statusMessage,
111+
text: async () => data
112+
});
113+
});
114+
});
115+
116+
req.on('error', reject);
117+
req.end();
118+
});
119+
};
120+
}
121+
122+
fetchSwagger();

0 commit comments

Comments
 (0)