apigrip 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  if (t) document.documentElement.setAttribute('data-theme', t);
11
11
  } catch (e) {}
12
12
  </script>
13
- <script type="module" crossorigin src="/assets/index-B6sjbmFk.js"></script>
13
+ <script type="module" crossorigin src="/assets/index--ZL-pHkt.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/assets/index-kzeRjfI8.css">
15
15
  </head>
16
16
  <body>
package/mcp/server.js CHANGED
@@ -14,15 +14,10 @@ import { saveLastResponse, loadLastResponse } from '../core/response-store.js';
14
14
  /**
15
15
  * Create and configure the MCP server.
16
16
  *
17
- * @param {object} options
18
- * @param {string} options.projectDir - Absolute path to the project directory
19
- * @param {string} options.specPath - Absolute path to the spec file
20
- * @param {object} options.spec - Parsed and dereferenced OpenAPI spec object
17
+ * @param {object} state - Mutable state object with projectDir, specPath, spec
21
18
  * @returns {Promise<McpServer>}
22
19
  */
23
- export async function createMcpServer(options = {}) {
24
- const { projectDir, specPath, spec } = options;
25
-
20
+ export async function createMcpServer(state) {
26
21
  const server = new McpServer({
27
22
  name: 'apigrip',
28
23
  version: '0.1.0',
@@ -35,20 +30,20 @@ export async function createMcpServer(options = {}) {
35
30
  contents: [{
36
31
  uri: uri.href,
37
32
  mimeType: 'application/json',
38
- text: JSON.stringify(spec, null, 2),
33
+ text: JSON.stringify(state.spec, null, 2),
39
34
  }],
40
35
  };
41
36
  });
42
37
 
43
38
  server.resource('project-info', 'project://info', async (uri) => {
44
- const git = await getGitInfo(projectDir);
39
+ const git = await getGitInfo(state.projectDir);
45
40
  return {
46
41
  contents: [{
47
42
  uri: uri.href,
48
43
  mimeType: 'application/json',
49
44
  text: JSON.stringify({
50
- path: projectDir,
51
- spec_file: specPath,
45
+ path: state.projectDir,
46
+ spec_file: state.specPath,
52
47
  git_branch: git?.branch || null,
53
48
  git_commit: git?.commit || null,
54
49
  git_dirty: git?.dirty || null,
@@ -60,22 +55,22 @@ export async function createMcpServer(options = {}) {
60
55
  // ── Tools ──────────────────────────────────────────────────────────
61
56
 
62
57
  server.tool('get_project_path', 'Get the project directory path', {}, async () => {
63
- return { content: [{ type: 'text', text: projectDir }] };
58
+ return { content: [{ type: 'text', text: state.projectDir }] };
64
59
  });
65
60
 
66
61
  server.tool('get_spec_path', 'Get the spec file path', {}, async () => {
67
- return { content: [{ type: 'text', text: specPath }] };
62
+ return { content: [{ type: 'text', text: state.specPath }] };
68
63
  });
69
64
 
70
65
  server.tool('list_endpoints', 'List all API endpoints', {}, async () => {
71
- const endpoints = extractEndpoints(spec);
66
+ const endpoints = extractEndpoints(state.spec);
72
67
  return { content: [{ type: 'text', text: JSON.stringify(endpoints, null, 2) }] };
73
68
  });
74
69
 
75
70
  server.tool('get_endpoint', 'Get endpoint details',
76
71
  { method: z.string(), path: z.string() },
77
72
  async ({ method, path }) => {
78
- const details = getEndpointDetails(spec, method.toUpperCase(), path);
73
+ const details = getEndpointDetails(state.spec, method.toUpperCase(), path);
79
74
  if (!details) {
80
75
  return {
81
76
  content: [{ type: 'text', text: `Endpoint not found: ${method} ${path}` }],
@@ -89,7 +84,7 @@ export async function createMcpServer(options = {}) {
89
84
  server.tool('get_schema', 'Get a named schema',
90
85
  { name: z.string() },
91
86
  async ({ name }) => {
92
- const schemas = spec.components?.schemas || spec.definitions || {};
87
+ const schemas = state.spec.components?.schemas || state.spec.definitions || {};
93
88
  const schema = schemas[name];
94
89
  if (!schema) {
95
90
  const available = Object.keys(schemas);
@@ -103,7 +98,7 @@ export async function createMcpServer(options = {}) {
103
98
  );
104
99
 
105
100
  server.tool('list_environments', 'List environments', {}, async () => {
106
- const envData = loadEnvironments(projectDir);
101
+ const envData = loadEnvironments(state.projectDir);
107
102
  return {
108
103
  content: [{
109
104
  type: 'text',
@@ -119,10 +114,10 @@ export async function createMcpServer(options = {}) {
119
114
  { name: z.string().optional() },
120
115
  async ({ name } = {}) => {
121
116
  if (!name) {
122
- const resolved = resolveEnvironment(projectDir);
117
+ const resolved = resolveEnvironment(state.projectDir);
123
118
  return { content: [{ type: 'text', text: JSON.stringify(resolved, null, 2) }] };
124
119
  }
125
- const envData = loadEnvironments(projectDir);
120
+ const envData = loadEnvironments(state.projectDir);
126
121
  const env = envData.environments[name] || envData.base;
127
122
  return { content: [{ type: 'text', text: JSON.stringify(env, null, 2) }] };
128
123
  }
@@ -131,7 +126,7 @@ export async function createMcpServer(options = {}) {
131
126
  server.tool('validate_request_body', 'Validate a request body against the spec',
132
127
  { method: z.string(), path: z.string(), body: z.string() },
133
128
  async ({ method, path, body }) => {
134
- const details = getEndpointDetails(spec, method.toUpperCase(), path);
129
+ const details = getEndpointDetails(state.spec, method.toUpperCase(), path);
135
130
  if (!details) {
136
131
  return {
137
132
  content: [{ type: 'text', text: `Endpoint not found: ${method} ${path}` }],
@@ -158,7 +153,7 @@ export async function createMcpServer(options = {}) {
158
153
  async ({ method, path: endpointPath, params_json }) => {
159
154
  const params = params_json ? JSON.parse(params_json) : {};
160
155
  const upperMethod = method.toUpperCase();
161
- const details = getEndpointDetails(spec, upperMethod, endpointPath);
156
+ const details = getEndpointDetails(state.spec, upperMethod, endpointPath);
162
157
  if (!details) {
163
158
  return {
164
159
  content: [{ type: 'text', text: `Endpoint not found: ${method} ${endpointPath}` }],
@@ -169,7 +164,7 @@ export async function createMcpServer(options = {}) {
169
164
  // Resolve environment
170
165
  let env = {};
171
166
  try {
172
- const envData = loadEnvironments(projectDir);
167
+ const envData = loadEnvironments(state.projectDir);
173
168
  env = resolveEnvironment(envData);
174
169
  } catch {
175
170
  // Continue without environment
@@ -183,7 +178,7 @@ export async function createMcpServer(options = {}) {
183
178
  const body = resolvedParams.body != null ? resolvedParams.body : null;
184
179
 
185
180
  // Build URL
186
- const servers = details.servers || spec.servers || [];
181
+ const servers = details.servers || state.spec.servers || [];
187
182
  const url = buildUrl(servers, 0, {}, endpointPath, pathParams, queryParams, env.base_url);
188
183
 
189
184
  // Build curl args
@@ -204,7 +199,7 @@ export async function createMcpServer(options = {}) {
204
199
 
205
200
  // Validate response against spec
206
201
  const validation = validateResponse(
207
- spec,
202
+ state.spec,
208
203
  upperMethod,
209
204
  endpointPath,
210
205
  result.status,
@@ -224,7 +219,7 @@ export async function createMcpServer(options = {}) {
224
219
 
225
220
  // Cache response for later retrieval
226
221
  try {
227
- saveLastResponse(projectDir, upperMethod, endpointPath, response);
222
+ saveLastResponse(state.projectDir, upperMethod, endpointPath, response);
228
223
  } catch {
229
224
  // Non-critical
230
225
  }
@@ -242,7 +237,7 @@ export async function createMcpServer(options = {}) {
242
237
  server.tool('get_last_response', 'Get the last cached response for an endpoint',
243
238
  { method: z.string(), path: z.string() },
244
239
  async ({ method, path: endpointPath }) => {
245
- const cached = loadLastResponse(projectDir, method.toUpperCase(), endpointPath);
240
+ const cached = loadLastResponse(state.projectDir, method.toUpperCase(), endpointPath);
246
241
  if (!cached) {
247
242
  return {
248
243
  content: [{ type: 'text', text: `No cached response for ${method} ${endpointPath}` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apigrip",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "A spec-first, read-only OpenAPI client for developers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",
package/server/index.js CHANGED
@@ -40,6 +40,9 @@ export function createServer(options = {}) {
40
40
  }
41
41
  app.use('/api', createEventsRoutes(state));
42
42
 
43
+ // No favicon — return empty 204 to avoid SPA fallback noise
44
+ app.get('/favicon.ico', (req, res) => res.status(204).end());
45
+
43
46
  // Serve frontend static files
44
47
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
48
  const clientDist = path.resolve(__dirname, '../client/dist');
@@ -92,20 +92,20 @@ function diffEndpoints(oldEndpoints, newEndpoints) {
92
92
  * @param {Function} broadcast - broadcast(state, event, data) function.
93
93
  */
94
94
  export async function createSpecWatcher(state, broadcast) {
95
- if (!state.specFile) return;
95
+ if (!state.specPath) return;
96
96
 
97
- const filesToWatch = [state.specFile];
97
+ const filesToWatch = [state.specPath];
98
98
 
99
99
  // Add $ref dependency files
100
100
  try {
101
- const deps = await getRefDeps(state.specFile);
101
+ const deps = await getRefDeps(state.specPath);
102
102
  filesToWatch.push(...deps);
103
103
  } catch {
104
104
  // If we can't resolve deps, just watch the main file
105
105
  }
106
106
 
107
107
  // Git metadata files
108
- const gitDir = path.join(state.projectPath, '.git');
108
+ const gitDir = path.join(state.projectDir, '.git');
109
109
  const gitPaths = [
110
110
  path.join(gitDir, 'HEAD'),
111
111
  path.join(gitDir, 'index'),
@@ -133,7 +133,7 @@ export async function createSpecWatcher(state, broadcast) {
133
133
  // Debounced spec reload handler
134
134
  const handleSpecChange = debounce(async () => {
135
135
  try {
136
- const newSpec = await parseSpec(state.specFile);
136
+ const newSpec = await parseSpec(state.specPath);
137
137
  const newEndpoints = extractEndpoints(newSpec);
138
138
  const changed = diffEndpoints(currentEndpoints, newEndpoints);
139
139
 
@@ -147,7 +147,7 @@ export async function createSpecWatcher(state, broadcast) {
147
147
 
148
148
  // Re-watch any new $ref deps
149
149
  try {
150
- const newDeps = await getRefDeps(state.specFile);
150
+ const newDeps = await getRefDeps(state.specPath);
151
151
  for (const dep of newDeps) {
152
152
  watcher.add(dep);
153
153
  }
@@ -165,7 +165,7 @@ export async function createSpecWatcher(state, broadcast) {
165
165
  // Debounced git change handler
166
166
  const handleGitChange = debounce(async () => {
167
167
  try {
168
- const newGit = await getGitInfo(state.projectPath);
168
+ const newGit = await getGitInfo(state.projectDir);
169
169
  state.git = newGit;
170
170
  broadcast(state, 'git:changed', newGit);
171
171
  } catch {
@@ -199,10 +199,10 @@ export async function createSpecWatcher(state, broadcast) {
199
199
  // Handle unlink (deleted files)
200
200
  watcher.on('unlink', (changedPath) => {
201
201
  const resolved = path.resolve(changedPath);
202
- if (resolved === path.resolve(state.specFile)) {
202
+ if (resolved === path.resolve(state.specPath)) {
203
203
  // Main spec file deleted — broadcast error and tear down watcher
204
204
  broadcast(state, 'spec:error', {
205
- errors: ['Spec file deleted: ' + state.specFile],
205
+ errors: ['Spec file deleted: ' + state.specPath],
206
206
  partial: false,
207
207
  });
208
208
  state.spec = null;