Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/ai-assistants.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ https://the-codegen-project.org/api/mcp
## Self-Hosting

You can run your own instance of the MCP server for development or private use. See the [MCP server repository](https://github.com/the-codegen-project/cli/tree/main/mcp-server) for setup instructions.

## Q&A

### Q: If AI can generate code, why use The Codegen Project?
The generator gives you deterministic, repeatable output from a stable input (spec + config). That makes CI consistent, supports large-team conventions, and lets you regenerate code without drift across runs or models.

### Q: Won’t AI output be maintained by developers anyway?
Yes, but generators keep a traceable “source of truth” in the configuration. That means you can explain why code exists, regenerate it reliably, and keep updates consistent across many services.

### Q: When should I prefer AI over a generator?
Use AI for exploration, prototypes, or one-off scripts. Use the generator when you want consistent output, shared conventions, automated regeneration.

### Q: Does this project compete with AI assistants?
It complements them. The MCP server gives assistants a deterministic interface to create and adjust configs, while the generator produces the exact code your repo expects.

### Q: What’s the biggest advantage over “just using AI”?
Repeatability. The same input produces the same output every time, which is critical for CI, refactors, and multi-team consistency.
14 changes: 7 additions & 7 deletions mcp-server/lib/resources/bundled-docs.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion mcp-server/lib/tools/config-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
/**
* Create a codegen configuration file
*/
export function createConfig(input: CreateConfigInput): {

Check warning on line 44 in mcp-server/lib/tools/config-tools.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

Refactor this function to reduce its Cognitive Complexity from 18 to the 15 allowed
content: string;
filename: string;
warnings: string[];
Expand Down Expand Up @@ -173,7 +173,7 @@
return ` ${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`;
})
.join('\n');
return ` - ${lines.replace(/^ /, '')}`;
return ` - ${lines.replace(/^ {4}/, '')}`;
})
.join('\n');

Expand Down Expand Up @@ -349,7 +349,7 @@
/**
* Validate a configuration object
*/
export function validateConfig(input: ValidateConfigInput): {

Check warning on line 352 in mcp-server/lib/tools/config-tools.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

Refactor this function to reduce its Cognitive Complexity from 27 to the 15 allowed
valid: boolean;
errors: string[];
warnings: string[];
Expand Down Expand Up @@ -377,7 +377,7 @@
errors.push('generators must be an array');
} else {
const inputType = config.inputType as InputType;
const availablePresets = inputType ? inputTypeGenerators[inputType] : [];

Check warning on line 380 in mcp-server/lib/tools/config-tools.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

Generic Object Injection Sink

(config.generators as Array<Record<string, unknown>>).forEach((gen, i) => {
if (!gen.preset) {
Expand All @@ -386,7 +386,7 @@
const preset = gen.preset as GeneratorPreset;

// Check if preset is valid
if (!generators[preset]) {

Check warning on line 389 in mcp-server/lib/tools/config-tools.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

Generic Object Injection Sink
errors.push(`Generator at index ${i}: unknown preset "${preset}"`);
} else if (inputType && !availablePresets.includes(preset)) {
errors.push(
Expand Down
12 changes: 12 additions & 0 deletions src/codegen/generators/typescript/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
ChannelFunctionTypes
} from './types';
import {generateTypeScriptChannelsForAsyncAPI} from './asyncapi';
import {generateTypeScriptChannelsForOpenAPI} from './openapi';
export {
TypeScriptChannelRenderedFunctionType,
TypeScriptChannelRenderType,
Expand Down Expand Up @@ -72,6 +73,17 @@ export async function generateTypeScriptChannels(
externalProtocolFunctionInformation,
protocolDependencies
);
} else if (context.inputType === 'openapi') {
await generateTypeScriptChannelsForOpenAPI(
context,
parameters,
payloads,
headers,
protocolsToUse,
protocolCodeFunctions,
externalProtocolFunctionInformation,
protocolDependencies
);
}

return await finalizeGeneration(
Expand Down
232 changes: 232 additions & 0 deletions src/codegen/generators/typescript/channels/openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/**
* Generates TypeScript HTTP client functions from OpenAPI specifications.
* Maps OpenAPI paths and operations to the existing renderHttpFetchClient infrastructure.
*/
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
import {TypeScriptParameterRenderType} from '../parameters';
import {TypeScriptPayloadRenderType} from '../payloads';
import {TypeScriptHeadersRenderType} from '../headers';
import {
ChannelFunctionTypes,
TypeScriptChannelRenderedFunctionType,
SupportedProtocols,
TypeScriptChannelsContext
} from './types';
import {ConstrainedObjectModel} from '@asyncapi/modelina';
import {collectProtocolDependencies} from './utils';
import {resetHttpCommonTypesState} from './protocols/http';
import {renderHttpFetchClient, renderHttpCommonTypes} from './protocols/http/fetch';
import {getMessageTypeAndModule} from './utils';
import {pascalCase} from '../utils';

type OpenAPIDocument =
| OpenAPIV3.Document
| OpenAPIV2.Document
| OpenAPIV3_1.Document;
type HttpMethod =
| 'get'
| 'post'
| 'put'
| 'patch'
| 'delete'
| 'options'
| 'head';

const HTTP_METHODS: HttpMethod[] = [
'get',
'post',
'put',
'patch',
'delete',
'options',
'head'
];
const METHODS_WITH_BODY: HttpMethod[] = ['post', 'put', 'patch'];

// Track whether common types have been generated
let httpCommonTypesGenerated = false;

/**
* Generates TypeScript HTTP client channels from an OpenAPI document.
* Only supports http_client protocol - other protocols are ignored for OpenAPI input.
*/
export async function generateTypeScriptChannelsForOpenAPI(
context: TypeScriptChannelsContext,
parameters: TypeScriptParameterRenderType,
payloads: TypeScriptPayloadRenderType,
headers: TypeScriptHeadersRenderType,
protocolsToUse: SupportedProtocols[],
protocolCodeFunctions: Record<string, string[]>,
externalProtocolFunctionInformation: Record<
string,
TypeScriptChannelRenderedFunctionType[]
>,
protocolDependencies: Record<string, string[]>
): Promise<void> {
// Only http_client is supported for OpenAPI
if (!protocolsToUse.includes('http_client')) {
return;
}

// Reset HTTP common types state
resetHttpCommonTypesState();
httpCommonTypesGenerated = false;

const {openapiDocument} = validateOpenAPIContext(context);

// Collect dependencies
const deps = protocolDependencies['http_client'];
collectProtocolDependencies(payloads, parameters, headers, context, deps);

const renders: ReturnType<typeof renderHttpFetchClient>[] = [];

// Iterate OpenAPI paths
for (const [path, pathItem] of Object.entries(openapiDocument.paths ?? {})) {
if (!pathItem) continue;

for (const method of HTTP_METHODS) {
const operation = (pathItem as Record<string, unknown>)[method] as
| OpenAPIV3.OperationObject
| OpenAPIV2.OperationObject
| OpenAPIV3_1.OperationObject
| undefined;
if (!operation) continue;

const operationId = getOperationId(operation, method, path);
const hasBody = METHODS_WITH_BODY.includes(method);

// Look up payloads
const requestPayload = hasBody
? payloads.operationModels[operationId]
: undefined;
const responsePayload = payloads.operationModels[`${operationId}_Response`];

// Look up parameters
const parameterModel = parameters.channelModels[operationId];

// Extract status codes from responses
const statusCodes = extractStatusCodes(operation.responses);

// Get message types - handle undefined payloads
const requestMessageInfo = requestPayload
? getMessageTypeAndModule(requestPayload)
: {messageModule: undefined, messageType: undefined};
const responseMessageInfo = responsePayload
? getMessageTypeAndModule(responsePayload)
: {messageModule: undefined, messageType: undefined};

const {messageModule: requestMessageModule, messageType: requestMessageType} =
requestMessageInfo;
const {messageModule: replyMessageModule, messageType: replyMessageType} =
responseMessageInfo;

// Skip if no response type (nothing to generate)
if (!replyMessageType) continue;

// Generate the HTTP client function
const render = renderHttpFetchClient({
subName: pascalCase(operationId),
requestMessageModule: hasBody ? requestMessageModule : undefined,
requestMessageType: hasBody ? requestMessageType : undefined,
replyMessageModule,
replyMessageType,
requestTopic: path,
method: method.toUpperCase() as
| 'GET'
| 'POST'
| 'PUT'
| 'PATCH'
| 'DELETE'
| 'OPTIONS'
| 'HEAD',
statusCodes,
channelParameters: parameterModel?.model as
| ConstrainedObjectModel
| undefined
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAPI servers not extracted, defaults to localhost

Medium Severity

The renderHttpFetchClient call doesn't pass the servers parameter, even though OpenAPI documents define server URLs (e.g., http://petstore.swagger.io/v2 in the test spec). The generated HTTP clients default to 'localhost:3000' instead of the actual API server from the spec. Users must manually override the server for every request, defeating the purpose of defining servers in the OpenAPI document.

Fix in Cursor Fix in Web


renders.push(render);
}
}

// Generate common types once
if (!httpCommonTypesGenerated && renders.length > 0) {
const commonTypesCode = renderHttpCommonTypes();
protocolCodeFunctions['http_client'].unshift(commonTypesCode);
httpCommonTypesGenerated = true;
}

// Add renders to output
protocolCodeFunctions['http_client'].push(...renders.map((r) => r.code));
externalProtocolFunctionInformation['http_client'].push(
...renders.map((r) => ({
functionType: r.functionType,
functionName: r.functionName,
messageType: r.messageType ?? '',
replyType: r.replyType,
parameterType: undefined
}))
);

// Add dependencies
const renderedDeps = renders.flatMap((r) => r.dependencies);
deps.push(...new Set(renderedDeps));
}

/**
* Validates the context is for OpenAPI input and has a parsed document.
*/
function validateOpenAPIContext(context: TypeScriptChannelsContext): {
openapiDocument: OpenAPIDocument;
} {
const {openapiDocument, inputType} = context;
if (inputType !== 'openapi') {
throw new Error('Expected OpenAPI input, was not given');
}
if (!openapiDocument) {
throw new Error('Expected a parsed OpenAPI document, was not given');
}
return {openapiDocument};
}

/**
* Gets the operation ID from an OpenAPI operation.
* Falls back to generating one from method+path if not present.
*/
function getOperationId(
operation:
| OpenAPIV3.OperationObject
| OpenAPIV2.OperationObject
| OpenAPIV3_1.OperationObject,
method: string,
path: string
): string {
if (operation.operationId) {
return operation.operationId;
}
// Generate from method + path
const sanitizedPath = path.replace(/[^a-zA-Z0-9]/g, '');
return `${method}${sanitizedPath}`;
}

/**
* Extracts status codes from OpenAPI responses object.
*/
function extractStatusCodes(
responses:
| OpenAPIV3.ResponsesObject
| OpenAPIV2.ResponsesObject
| OpenAPIV3_1.ResponsesObject
| undefined
): {code: number; description: string; messageModule?: string; messageType?: string}[] {
if (!responses) return [];

return Object.entries(responses)
.filter(([code]) => code !== 'default' && !isNaN(Number(code)))
.map(([code, response]) => ({
code: Number(code),
description:
(response as OpenAPIV3.ResponseObject | OpenAPIV2.ResponseObject)
.description ?? 'Unknown'
}));
}
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,7 @@ function generateFunctionImplementation(params: {

// Generate response parsing
const responseParseCode = replyMessageModule
? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(rawData, response.status);`
? `const responseData = ${replyMessageModule}.unmarshal(rawData);`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid TypeScript generated for array response types

High Severity

When an OpenAPI operation returns an array type (like Pet[]), the generated code produces invalid TypeScript: Pet[].unmarshal(rawData). This is syntactically invalid because you cannot call methods on type literals. The snapshot at line 1330 confirms this generates non-compiling code. This affects any OpenAPI GET operation returning arrays.

Additional Locations (1)

Fix in Cursor Fix in Web

: `const responseData = ${replyMessageType}.unmarshal(rawData);`;

// Generate default context for optional context parameter
Expand Down
15 changes: 12 additions & 3 deletions src/codegen/inputs/openapi/generators/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,14 +398,23 @@ function generateUrlSerializationMethod(
*/
serializeUrl(basePath: string): string {
let url = basePath;

// Replace path parameters
${pathLogic}

// Add query parameters
${queryLogic}

return url;
}

/**
* Get the channel path with parameters substituted (compatible with AsyncAPI channel interface)
* @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}')
* @returns The path with parameters replaced
*/
getChannelWithParameters(basePath: string): string {
return this.serializeUrl(basePath);
}`;
}

Expand Down
Loading
Loading