Skip to content

Commit f71c121

Browse files
CopilotEMaher
andauthored
fix: address PR review comments — revert api-version default, remove MCP embedding, add ServicePolicy test
Agent-Logs-Url: https://github.com/Azure/apiops-cli/sessions/8711e436-2a22-4c8b-b435-5f7a7e17847a Co-authored-by: EMaher <9244742+EMaher@users.noreply.github.com>
1 parent e029744 commit f71c121

6 files changed

Lines changed: 60 additions & 74 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli/extract-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async function executeExtract(
7575
}
7676

7777
// Build service context
78-
const apiVersion = globalOpts.apiVersion ?? process.env.AZURE_API_VERSION ?? '2024-10-01-preview';
78+
const apiVersion = globalOpts.apiVersion ?? process.env.AZURE_API_VERSION ?? '2024-05-01';
7979
const cloudName = globalOpts.cloud ?? 'public';
8080
const cloudConfig = getCloudConfig(cloudName);
8181
const baseUrl = buildArmBaseUrl(cloudName, subscriptionId, options.resourceGroup, options.serviceName);

src/cli/publish-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ async function executePublish(
8383

8484
// Build service context
8585
const apiVersion =
86-
globalOpts.apiVersion ?? process.env.AZURE_API_VERSION ?? '2024-10-01-preview';
86+
globalOpts.apiVersion ?? process.env.AZURE_API_VERSION ?? '2024-05-01';
8787
const cloudName = globalOpts.cloud ?? 'public';
8888
const cloudConfig = getCloudConfig(cloudName);
8989
const baseUrl = buildArmBaseUrl(

src/services/api-publisher.ts

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,7 @@ export async function publishApi(
5959

6060
// Step 3: Publish child resources in parallel
6161
// When a spec was imported, operations and schemas are auto-created by APIM
62-
await publishApiChildren(
63-
client,
64-
store,
65-
context,
66-
descriptor,
67-
config,
68-
rootResult.specImported,
69-
rootResult.embeddedMcpApplied,
70-
);
62+
await publishApiChildren(client, store, context, descriptor, config, rootResult.specImported);
7163

7264
return {
7365
descriptor,
@@ -111,26 +103,6 @@ function getImportFormat(specFormat: string, _apiType?: string): string | undefi
111103
interface RootApiResult {
112104
status: 'success' | 'skipped';
113105
specImported: boolean;
114-
embeddedMcpApplied: boolean;
115-
}
116-
117-
function getEmbeddedMcpProperties(
118-
json: Record<string, unknown> | undefined
119-
): Record<string, unknown> | undefined {
120-
const properties = json?.properties as Record<string, unknown> | undefined;
121-
if (!properties) {
122-
return undefined;
123-
}
124-
125-
const embeddedProperties: Record<string, unknown> = {};
126-
if (properties.mcpProperties !== undefined) {
127-
embeddedProperties.mcpProperties = properties.mcpProperties;
128-
}
129-
if (properties.mcpTools !== undefined) {
130-
embeddedProperties.mcpTools = properties.mcpTools;
131-
}
132-
133-
return Object.keys(embeddedProperties).length > 0 ? embeddedProperties : undefined;
134106
}
135107

136108
/**
@@ -153,33 +125,12 @@ async function publishRootApi(
153125
status: 'skipped',
154126
action: 'noop',
155127
specImported: false,
156-
embeddedMcpApplied: false,
157128
};
158129
}
159130

160131
// Apply overrides
161132
json = applyOverrides(descriptor, json, config.overrides);
162133

163-
const embeddedMcpProperties = getEmbeddedMcpProperties(
164-
await store.readResource(config.sourceDir, {
165-
type: ResourceType.McpServer,
166-
nameParts: [...descriptor.nameParts],
167-
workspace: descriptor.workspace,
168-
})
169-
);
170-
const embeddedMcpApplied = embeddedMcpProperties !== undefined;
171-
if (embeddedMcpApplied) {
172-
const properties = json.properties as Record<string, unknown> | undefined;
173-
json = {
174-
...json,
175-
properties: {
176-
...(properties ?? {}),
177-
...embeddedMcpProperties,
178-
},
179-
};
180-
logger.debug(`Merged MCP artifact into API payload for "${getNamePart(descriptor.nameParts, 0)}"`);
181-
}
182-
183134
// Try to read the specification file for this API
184135
let specImported = false;
185136
const specResult = await store.readContent(config.sourceDir, descriptor, 'specification');
@@ -227,7 +178,6 @@ async function publishRootApi(
227178
status: 'success',
228179
action: 'put',
229180
specImported,
230-
embeddedMcpApplied,
231181
};
232182
}
233183

@@ -288,8 +238,7 @@ async function publishApiChildren(
288238
context: ApimServiceContext,
289239
apiDescriptor: ResourceDescriptor,
290240
config: PublishConfig,
291-
specImported: boolean = false,
292-
embeddedMcpApplied: boolean = false
241+
specImported: boolean = false
293242
): Promise<void> {
294243
// List all resources from store
295244
const allDescriptors = await store.listResources(config.sourceDir);
@@ -299,8 +248,7 @@ async function publishApiChildren(
299248
(d) =>
300249
API_CHILD_TYPES.includes(d.type) &&
301250
getNamePart(d.nameParts, 0) === getNamePart(apiDescriptor.nameParts, 0) &&
302-
!(specImported && SPEC_MANAGED_CHILD_TYPES.has(d.type)) &&
303-
!(embeddedMcpApplied && d.type === ResourceType.McpServer)
251+
!(specImported && SPEC_MANAGED_CHILD_TYPES.has(d.type))
304252
);
305253

306254
// When a spec was imported, APIM auto-creates operations and schemas but loses

tests/unit/services/api-publisher.test.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ describe('api-publisher', () => {
101101
);
102102
});
103103

104-
it('should merge embedded MCP artifact data into the root API payload', async () => {
104+
it('should publish McpServer as a standard child resource without merging into root API payload', async () => {
105105
const client = createMockClient();
106-
const store = createMockStore([]);
106+
const mcpChild = { type: ResourceType.McpServer, nameParts: ['orders-api'] };
107+
const store = createMockStore([mcpChild]);
107108
store.readResource.mockImplementation(async (_sourceDir: string, descriptor: ResourceDescriptor) => {
108109
if (descriptor.type === ResourceType.Api) {
109110
return { name: descriptor.nameParts[0] ?? '', properties: { type: 'mcp' } };
@@ -127,20 +128,13 @@ describe('api-publisher', () => {
127128

128129
await publishApi(client, store, testContext, apiDescriptor, testConfig);
129130

130-
expect(client.putResource).toHaveBeenCalledWith(
131-
testContext,
132-
apiDescriptor,
133-
expect.objectContaining({
134-
properties: expect.objectContaining({
135-
type: 'mcp',
136-
mcpProperties: { serverUrl: 'https://example.com/mcp' },
137-
mcpTools: [{ name: 'invokeTool' }],
138-
}),
139-
})
140-
);
131+
// Root API PUT should NOT contain MCP properties — they stay in the McpServer child
132+
const [, , payload] = client.putResource.mock.calls[0] as [unknown, unknown, Record<string, unknown>];
133+
const properties = payload.properties as Record<string, unknown> | undefined;
134+
expect(properties).not.toHaveProperty('mcpProperties');
141135
});
142136

143-
it('should skip child MCP publish when MCP artifact was merged into the root API payload', async () => {
137+
it('should include McpServer child in publish tasks (standard child, not skipped)', async () => {
144138
const client = createMockClient();
145139
const children = [
146140
{ type: ResourceType.McpServer, nameParts: ['orders-api'] },
@@ -168,7 +162,8 @@ describe('api-publisher', () => {
168162
await publishApi(client, store, testContext, apiDescriptor, testConfig);
169163

170164
const tasks = mockRunParallel.mock.calls[0][0] as Array<() => Promise<unknown>>;
171-
expect(tasks).toHaveLength(1);
165+
// Both McpServer and ApiPolicy should be included — McpServer is a standard child
166+
expect(tasks).toHaveLength(2);
172167
});
173168

174169
it('should return failed result when root API publish fails', async () => {

tests/unit/services/publish-service.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,49 @@ describe('publish-service', () => {
326326
expect(result.totalPuts).toBe(2);
327327
expect(client.putResource).toHaveBeenCalledTimes(2);
328328
});
329+
330+
it('should not throw RangeError when ServicePolicy (nameParts=[]) is in the batch', async () => {
331+
// ServicePolicy is a tier-2 top-level singleton with no name segments (nameParts: []).
332+
// The isTopLevelSingleton() guard in executePuts must fire so that
333+
// getNamePart(d.nameParts, 0) is never called for this resource.
334+
const resources: ResourceDescriptor[] = [
335+
{ type: ResourceType.ServicePolicy, nameParts: [] },
336+
{ type: ResourceType.Api, nameParts: ['my-api'] },
337+
];
338+
339+
const client = createMockClient();
340+
const store = createMockStore(resources);
341+
// ServicePolicy is a policy type — its artifact is policy.xml, not JSON.
342+
// Mock readContent so the policy publish path reaches putResource.
343+
vi.mocked(store.readContent).mockImplementation(
344+
async (_sourceDir, descriptor, contentType) => {
345+
if (
346+
descriptor.type === ResourceType.ServicePolicy &&
347+
contentType === 'policy'
348+
) {
349+
// Minimal valid APIM policy XML — content doesn't matter for this test,
350+
// only that readContent returns something so publishPolicy reaches putResource.
351+
return { content: '<policies><inbound/></policies>', format: 'xml' };
352+
}
353+
return undefined;
354+
}
355+
);
356+
357+
const config: PublishConfig = {
358+
service: testContext,
359+
sourceDir: '/source',
360+
dryRun: false,
361+
deleteUnmatched: false,
362+
logLevel: LogLevel.INFO,
363+
};
364+
365+
await expect(runPublish(client, store, config)).resolves.not.toThrow();
366+
expect(client.putResource).toHaveBeenCalledWith(
367+
testContext,
368+
expect.objectContaining({ type: ResourceType.ServicePolicy }),
369+
expect.anything(),
370+
);
371+
});
329372
});
330373

331374
describe('pre-flight validation', () => {

0 commit comments

Comments
 (0)