feat: WIP added tools to mcp and openapi origin adapters

pull/715/head
Travis Fischer 2025-05-30 01:38:23 +07:00
rodzic b07599972e
commit 8ed18982ad
53 zmienionych plików z 2880 dodań i 79 usunięć

Wyświetl plik

@ -26,6 +26,7 @@
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/platform": "workspace:*",
"@agentic/platform-core": "workspace:*",
"@agentic/platform-schemas": "workspace:*",
"@agentic/platform-validators": "workspace:*",

Wyświetl plik

@ -1,5 +1,5 @@
import { validateAgenticProjectConfig } from '@agentic/platform'
import { assert, parseZodSchema, sha256 } from '@agentic/platform-core'
import { validateAgenticProjectConfig } from '@agentic/platform-schemas'
import { validators } from '@agentic/platform-validators'
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'

Wyświetl plik

@ -28,6 +28,7 @@
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/platform": "workspace:*",
"@agentic/platform-api-client": "workspace:*",
"@agentic/platform-core": "workspace:*",
"@agentic/platform-schemas": "workspace:*",

Wyświetl plik

@ -24,6 +24,7 @@
"test:unit": "vitest run"
},
"dependencies": {
"@agentic/platform": "workspace:*",
"@agentic/platform-api-client": "workspace:*",
"@agentic/platform-core": "workspace:*",
"@agentic/platform-schemas": "workspace:*",

Wyświetl plik

@ -1,7 +1,5 @@
import {
type AgenticProjectConfig,
validateAgenticProjectConfig
} from '@agentic/platform-schemas'
import type { AgenticProjectConfig } from '@agentic/platform-schemas'
import { validateAgenticProjectConfig } from '@agentic/platform'
import { loadConfig } from 'unconfig'
export async function loadAgenticConfig({

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'Test Invalid Name 0',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'Test-Invalid-Name-1',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test_invalid_name_2',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
originUrl: 'https://jsonplaceholder.typicode.com'

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: '@foo/bar', // invalid; name contains invalid characters

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-invalid-origin-url-0',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-invalid-origin-url-1',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-invalid-origin-url-3',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-base-inconsistent',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-custom-inconsistent',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-duplicate-0',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-duplicate-1',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-empty-0',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-empty-1',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-empty-2',

Wyświetl plik

@ -25,6 +25,6 @@
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/platform-schemas": "workspace:*"
"@agentic/platform": "workspace:*"
}
}

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-basic-raw-free-ts',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-3-plans',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-custom-0',

Wyświetl plik

@ -1,4 +1,4 @@
import { defaultFreePricingPlan, defineConfig } from '@agentic/platform-schemas'
import { defaultFreePricingPlan, defineConfig } from '@agentic/platform'
export default defineConfig({
// TODO: resolve name / slug conflicts

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-monthly-annual',

Wyświetl plik

@ -1,4 +1,4 @@
import { defineConfig } from '@agentic/platform-schemas'
import { defineConfig } from '@agentic/platform'
export default defineConfig({
name: 'test-pricing-pay-as-you-go',

Wyświetl plik

@ -0,0 +1,61 @@
{
"openapi": "3.0.2",
"info": {
"title": "OpenAPI Mixed Test Fixture",
"version": "0.1.0"
},
"paths": {
"/echo/{id}": {
"parameters": [
{
"required": true,
"schema": {
"title": "id",
"type": "string"
},
"name": "id",
"in": "path"
}
],
"get": {
"summary": "Echo",
"operationId": "echo",
"parameters": [
{
"required": false,
"schema": {
"title": "x-custom-header",
"type": "string"
},
"name": "x-custom-header",
"in": "header"
},
{
"required": false,
"schema": {
"title": "name",
"type": "string"
},
"name": "name",
"in": "query"
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
},
"servers": [
{
"url": "https://test-openapi-basic.now.sh"
}
]
}

Wyświetl plik

@ -24,7 +24,10 @@
},
"dependencies": {
"@agentic/platform-core": "workspace:*",
"@redocly/openapi-core": "catalog:"
"@agentic/platform-schemas": "workspace:*",
"@redocly/openapi-core": "catalog:",
"camelcase": "^8.0.0",
"decamelize": "^6.0.0"
},
"publishConfig": {
"access": "public"

Wyświetl plik

@ -2,7 +2,6 @@
exports[`validateOpenAPISpec > basic.json (file url) 1`] = `
{
"components": {},
"info": {
"title": "OpenAPI Basic Test Fixture",
"version": "0.1.0",
@ -47,7 +46,6 @@ exports[`validateOpenAPISpec > basic.json (file url) 1`] = `
exports[`validateOpenAPISpec > basic.json (http url) 1`] = `
{
"components": {},
"info": {
"title": "OpenAPI Basic Test Fixture",
"version": "0.1.0",
@ -92,7 +90,6 @@ exports[`validateOpenAPISpec > basic.json (http url) 1`] = `
exports[`validateOpenAPISpec > basic.json (string) 1`] = `
{
"components": {},
"info": {
"title": "OpenAPI Basic Test Fixture",
"version": "0.1.0",

Wyświetl plik

@ -0,0 +1,71 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'vitest'
import { getToolsFromOpenAPISpec } from './get-tools-from-openapi-spec'
import { validateOpenAPISpec } from './validate-openapi-spec'
const validFixtures = [
'basic.json',
'mixed.json',
'firecrawl.json',
'open-meteo.yaml',
'pet-store.json',
'petstore-expanded.json',
'security.json',
'tic-tac-toe.json'
]
const invalidFixtures = ['notion.json']
const fixturesDir = path.join(
fileURLToPath(import.meta.url),
'..',
'..',
'fixtures'
)
describe('getToolsFromOpenAPISpec', () => {
for (const fixture of validFixtures) {
test(
fixture,
{
timeout: 60_000
},
async () => {
const fixturePath = path.join(fixturesDir, fixture)
const source = await readFile(fixturePath, 'utf8')
const spec = await validateOpenAPISpec(source, {
dereference: true
})
const result = await getToolsFromOpenAPISpec(spec)
expect(result).toMatchSnapshot()
}
)
}
for (const fixture of invalidFixtures) {
test(
`${fixture} (invalid)`,
{
timeout: 60_000
},
async () => {
const fixturePath = path.join(fixturesDir, fixture)
const source = await readFile(fixturePath, 'utf8')
const spec = await validateOpenAPISpec(source, {
dereference: true
})
await expect(async () =>
getToolsFromOpenAPISpec(spec)
).rejects.toThrowError()
}
)
}
})

Wyświetl plik

@ -0,0 +1,219 @@
import { assert, parseZodSchema } from '@agentic/platform-core'
import {
type OpenAPIOperationHttpMethod,
type OpenAPIOperationParameterSource,
type OpenAPIToolOperation,
openapiToolOperationSchema,
type Tool,
toolSchema
} from '@agentic/platform-schemas'
import decamelize from 'decamelize'
import type {
DereferencedLooseOpenAPI3Spec,
OperationObject,
ParameterObject,
SchemaObject
} from './types'
import { convertParametersToJsonSchema } from './openapi-parameters-to-json-schema'
import { camelCase, mergeJsonSchemaObjects } from './utils'
const jsonContentType = 'application/json'
const multipartFormData = 'multipart/form-data'
const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'trace'] as const
const paramSources = ['body', 'formData', 'header', 'path', 'query'] as const
/**
* Converts a fully dereferenced and validated OpenAPI spec into an array of
* MCP-compatible tools, with a map of tool names to their corresponding OpenAPI
* operations.
*
* This allows us to expose OpenAPI HTTP operations as MCP tools and convert
* MCP tool calls to corresponding HTTP requests.
*/
export async function getToolsFromOpenAPISpec(
spec: DereferencedLooseOpenAPI3Spec
): Promise<{
tools: Tool[]
toolToOperationMap: Record<string, OpenAPIToolOperation>
}> {
const tools: Tool[] = []
const toolToOperationMap: Record<string, OpenAPIToolOperation> = {}
const requestBodyJsonSchemaPaths = [
'requestBody',
'content',
jsonContentType,
'schema'
]
const requestBodyFormDataJsonSchemaPaths = [
'requestBody',
'content',
multipartFormData,
'schema'
]
const operationResponsePaths = [
['responses', '200', 'content', jsonContentType, 'schema'],
['responses', '201', 'content', jsonContentType, 'schema']
// ['responses', 'default', 'content', jsonContentType, 'schema']
]
const operationRequestPaths = [
requestBodyJsonSchemaPaths,
requestBodyFormDataJsonSchemaPaths
]
const operationNames = new Set<string>()
for (const path in spec.paths) {
const pathItem = spec.paths[path]
assert(pathItem)
// console.log(JSON.stringify(pathItem, null, 2))
const pathParamsJsonSchema = {
type: 'object',
properties: {} as Record<string, SchemaObject>,
required: [] as string[]
} satisfies SchemaObject
const pathParamsSources: Record<string, OpenAPIOperationParameterSource> =
{}
if (pathItem.parameters) {
const params = convertParametersToJsonSchema(
pathItem.parameters as ParameterObject[]
)
for (const source of paramSources) {
if (params[source]) {
mergeJsonSchemaObjects(pathParamsJsonSchema, params[source], {
source,
sources: pathParamsSources,
label: `path "${path}"`
})
}
}
}
for (const method of httpMethods) {
const operation = pathItem[method] as OperationObject
if (!operation) {
continue
}
const operationId =
operation.operationId || `${method}${path.replaceAll(/\W+/g, '_')}`
assert(
operationId,
`Invalid operation id "${operationId}" for OpenAPI path "${method} ${path}"`
)
const operationName = camelCase(operationId.replaceAll('/', '_'))
const operationNameSnakeCase = decamelize(operationName)
assert(
!operationNames.has(operationName),
`Duplicate operation name "${operationName}"`
)
operationNames.add(operationName)
const operationParamsJsonSchema = structuredClone(pathParamsJsonSchema)
const operationParamsSources: Record<
string,
OpenAPIOperationParameterSource
> = structuredClone(pathParamsSources)
const operationResponseJsonSchemas: Record<string, SchemaObject> = {}
for (const schemaPath of operationRequestPaths) {
let current: any = operation
for (const key of schemaPath) {
current = current[key]
if (!current) break
}
if (current) {
mergeJsonSchemaObjects(operationParamsJsonSchema, current, {
source: schemaPath[2] === jsonContentType ? 'body' : 'formData',
sources: operationParamsSources,
label: `operation "${operationId}"`
})
break
}
}
for (const schemaPath of operationResponsePaths) {
let current: any = operation
for (const key of schemaPath) {
current = current[key]
if (!current) break
}
if (current) {
const status = schemaPath[1]!
assert(
status,
`Invalid status ${status} for operation ${operationName}`
)
if (current.type !== 'object') {
// console.warn(
// `Invalid OpenAPI response type "${current.type}" for operation "${operationName}"`
// )
break
}
operationResponseJsonSchemas[status] = current
break
}
}
if (operation.parameters) {
const params = convertParametersToJsonSchema(
operation.parameters as ParameterObject[]
)
for (const source of paramSources) {
if (params[source]) {
mergeJsonSchemaObjects(pathParamsJsonSchema, params[source], {
source,
sources: operationParamsSources,
label: `operation "${operationId}"`
})
}
}
}
const operationResponseJsonSchema =
operationResponseJsonSchemas['200'] ||
operationResponseJsonSchemas['201']
const description = operation.description || operation.summary
const { tags } = operation
tools.push(
parseZodSchema(toolSchema, {
name: operationNameSnakeCase,
description,
inputSchema: operationParamsJsonSchema,
outputSchema: operationResponseJsonSchema
})
)
toolToOperationMap[operationNameSnakeCase] = parseZodSchema(
openapiToolOperationSchema,
{
operationId,
method: method.toLowerCase() as OpenAPIOperationHttpMethod,
path,
parameterSources: operationParamsSources,
tags
}
)
}
}
return {
tools,
toolToOperationMap
}
}

Wyświetl plik

@ -1,3 +1,4 @@
export * from './get-tools-from-openapi-spec'
export * from './redocly-config'
export type * from './types'
export * from './validate-openapi-spec'

Wyświetl plik

@ -0,0 +1,224 @@
/**
* This file is forked from: https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-jsonschema-parameters
*
* Several fixes have been applied.
*
* The original code is licensed under the MIT license.
*/
import type { ParameterObject, SchemaObject } from './types'
export interface OpenAPIParametersAsJsonSchema {
body?: SchemaObject
formData?: SchemaObject
header?: SchemaObject
path?: SchemaObject
query?: SchemaObject
cookie?: SchemaObject
}
const VALIDATION_KEYWORDS = new Set([
'additionalItems',
'default',
'example',
'description',
'enum',
'examples',
'exclusiveMaximum',
'exclusiveMinimum',
'format',
'items',
'maxItems',
'maxLength',
'maximum',
'minItems',
'minLength',
'minimum',
'multipleOf',
'pattern',
'title',
'type',
'uniqueItems'
])
const SUBSCHEMA_KEYWORDS = [
'additionalItems',
'items',
'contains',
'additionalProperties',
'propertyNames',
'not'
]
const SUBSCHEMA_ARRAY_KEYWORDS = ['items', 'allOf', 'anyOf', 'oneOf']
const SUBSCHEMA_OBJECT_KEYWORDS = [
'definitions',
'properties',
'patternProperties',
'dependencies'
]
export function convertParametersToJsonSchema(
parameters: ParameterObject[]
): OpenAPIParametersAsJsonSchema {
const parametersSchema: OpenAPIParametersAsJsonSchema = {}
const bodySchema = getBodySchema(parameters)
const formDataSchema = getSchema(parameters, 'formData')
const headerSchema = getSchema(parameters, 'header')
const pathSchema = getSchema(parameters, 'path')
const querySchema = getSchema(parameters, 'query')
const cookieSchema = getSchema(parameters, 'cookie')
if (bodySchema) {
parametersSchema.body = bodySchema
}
if (formDataSchema) {
parametersSchema.formData = formDataSchema
}
if (headerSchema) {
parametersSchema.header = headerSchema
}
if (pathSchema) {
parametersSchema.path = pathSchema
}
if (querySchema) {
parametersSchema.query = querySchema
}
if (cookieSchema) {
parametersSchema.cookie = cookieSchema
}
return parametersSchema
}
function copyValidationKeywords(src: any) {
const dst: any = {}
for (let i = 0, keys = Object.keys(src), len = keys.length; i < len; i++) {
const keyword = keys[i]
if (!keyword) continue
if (VALIDATION_KEYWORDS.has(keyword) || keyword.slice(0, 2) === 'x-') {
dst[keyword] = src[keyword]
}
}
return dst
}
function handleNullable(schema: SchemaObject) {
return { anyOf: [schema, { type: 'null' }] }
}
function handleNullableSchema(schema: any) {
if (typeof schema !== 'object' || schema === null) {
return schema
}
const newSchema = { ...schema }
for (const keyword of SUBSCHEMA_KEYWORDS) {
if (
typeof schema[keyword] === 'object' &&
schema[keyword] !== null &&
!Array.isArray(schema[keyword])
) {
newSchema[keyword] = handleNullableSchema(schema[keyword])
}
}
for (const keyword of SUBSCHEMA_ARRAY_KEYWORDS) {
if (Array.isArray(schema[keyword])) {
newSchema[keyword] = schema[keyword].map(handleNullableSchema)
}
}
for (const keyword of SUBSCHEMA_OBJECT_KEYWORDS) {
if (typeof schema[keyword] === 'object' && schema[keyword] !== null) {
newSchema[keyword] = { ...schema[keyword] }
for (const prop of Object.keys(schema[keyword])) {
newSchema[keyword][prop] = handleNullableSchema(schema[keyword][prop])
}
}
}
delete newSchema.$ref
if (schema.nullable) {
delete newSchema.nullable
return handleNullable(newSchema)
}
return newSchema
}
function getBodySchema(parameters: any[]) {
let bodySchema = parameters.find((param) => {
return param.in === 'body' && param.schema
})
if (bodySchema) {
bodySchema = bodySchema.schema
}
return bodySchema
}
function getSchema(parameters: any[], type: string) {
const params = parameters.filter(byIn(type))
let schema: any
if (params.length) {
schema = { type: 'object', properties: {} }
for (const param of params) {
let paramSchema = copyValidationKeywords(param)
if ('schema' in param) {
paramSchema = {
...paramSchema,
...handleNullableSchema(param.schema)
}
if ('examples' in param) {
paramSchema.examples = getExamples(param.examples)
}
schema.properties[param.name] = paramSchema
} else {
if ('examples' in paramSchema) {
paramSchema.examples = getExamples(paramSchema.examples)
}
schema.properties[param.name] = param.nullable
? handleNullable(paramSchema)
: paramSchema
}
}
schema.required = getRequiredParams(params)
}
return schema
}
function getRequiredParams(parameters: any[]) {
return parameters.filter(byRequired).map(toName)
}
function getExamples(exampleSchema: any) {
return Object.keys(exampleSchema).map((k) => exampleSchema[k].value)
}
function byIn(str: string) {
return (param: any) => param.in === str && param.type !== 'file'
}
function byRequired(param: any) {
return !!param.required
}
function toName(param: any) {
return param.name
}

Wyświetl plik

@ -624,3 +624,19 @@ export type SecurityRequirementObject = {
}
export type $defs = Record<string, SchemaObject>
type Deref<T> =
// 1. Remove any direct ReferenceObject
T extends ReferenceObject
? never
: // 2. Recurse into arrays/tuples
T extends readonly (infer U)[]
? readonly Deref<U>[]
: // 3. Recurse into object properties
T extends object
? { [K in keyof T]: Deref<T[K]> }
: // 4. Primitives, functions, etc. stay unchanged
T
/** LooseOpenAPI3Spec with every `$ref` wiped out */
export type DereferencedLooseOpenAPI3Spec = Deref<LooseOpenAPI3Spec>

Wyświetl plik

@ -0,0 +1,62 @@
import type { OpenAPIOperationParameterSource } from '@agentic/platform-schemas'
import { assert } from '@agentic/platform-core'
import camelCaseImpl from 'camelcase'
import type { ObjectSubtype, SchemaObject } from './types'
export function camelCase(identifier: string): string {
return camelCaseImpl(identifier)
}
export function mergeJsonSchemaObjects(
schema0: SchemaObject & ObjectSubtype,
schema1: SchemaObject,
{
source,
sources,
label
}: {
source: OpenAPIOperationParameterSource
sources: Record<string, OpenAPIOperationParameterSource>
label: string
}
) {
if (schema1.type === 'object' && schema1.properties) {
schema0.properties = {
...schema0.properties,
...schema1.properties
}
for (const key of Object.keys(schema1.properties)) {
assert(
!sources[key],
`Duplicate parameter "${key}" in OpenAPI spec ${label}`
)
sources[key] = source
}
}
if (schema1.required) {
schema0.required = Array.from(
new Set([...(schema0.required || []), ...schema1.required])
)
}
// https://community.openai.com/t/official-documentation-for-supported-schemas-for-response-format-parameter-in-calls-to-client-beta-chats-completions-parse/932422/3
// https://platform.openai.com/docs/guides/structured-outputs
// https://json-schema.org/understanding-json-schema/reference/combining
assert(
!schema1.oneOf,
`JSON schema "oneOf" is not supported in OpenAPI spec ${label}`
)
assert(
!schema1.allOf,
`JSON schema "oneOf" is not supported in OpenAPI spec ${label}`
)
// TODO: Support "anyOf" which should be supported by OpenAI function calling
assert(
!schema1.anyOf,
`JSON schema "anyOf" is not supported in OpenAPI spec ${label}`
)
}

Wyświetl plik

@ -12,7 +12,7 @@ import {
Source
} from '@redocly/openapi-core'
import type { LooseOpenAPI3Spec } from './types'
import type { DereferencedLooseOpenAPI3Spec, LooseOpenAPI3Spec } from './types'
import { getDefaultRedoclyConfig } from './redocly-config'
interface ParseSchemaOptions {
@ -27,20 +27,26 @@ interface ParseSchemaOptions {
*
* Adapted from https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-typescript/src/lib/redoc.ts
*/
export async function validateOpenAPISpec(
export async function validateOpenAPISpec<
TDereference extends boolean | undefined
>(
source: string | URL | Buffer | Record<string, unknown>,
{
cwd,
redoclyConfig,
logger = console,
silent = false
logger,
silent = false,
dereference = false
}: {
cwd?: URL
redoclyConfig?: RedoclyConfig
logger?: Logger
silent?: boolean
dereference?: TDereference
} = {}
): Promise<LooseOpenAPI3Spec> {
): Promise<
TDereference extends true ? DereferencedLooseOpenAPI3Spec : LooseOpenAPI3Spec
> {
if (!redoclyConfig) {
redoclyConfig = await getDefaultRedoclyConfig()
}
@ -95,9 +101,11 @@ export async function validateOpenAPISpec(
_processProblems(problems, { silent, logger })
const bundled = await bundle({
doc: document,
config: redoclyConfig,
dereference: false,
doc: document
dereference,
removeUnusedComponents: true,
externalRefResolver: resolver
})
_processProblems(bundled.problems, { silent, logger })
@ -195,7 +203,7 @@ function _processProblems(
logger,
silent
}: {
logger: Logger
logger?: Logger
silent: boolean
}
) {
@ -210,9 +218,9 @@ function _processProblems(
if (problem.severity === 'error') {
errorMessage = problemMessage
logger.error('openapi spec error', problemMessage)
logger?.error('openapi spec error', problemMessage)
} else if (!silent) {
logger.warn('openapi spec warning', problemMessage)
logger?.warn('openapi spec warning', problemMessage)
}
}

Wyświetl plik

@ -0,0 +1,43 @@
{
"name": "@agentic/platform",
"version": "0.0.1",
"description": "Public SDK for developers building on top of the Agentic platform.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"license": "UNLICENSED",
"repository": {
"type": "git",
"url": "git+https://github.com/transitive-bullshit/agentic-platform.git",
"directory": "packages/platform"
},
"type": "module",
"source": "./src/index.ts",
"types": "./src/index.ts",
"sideEffects": false,
"exports": {
".": "./src/index.ts"
},
"scripts": {
"test": "run-s test:*",
"test:lint": "eslint .",
"test:typecheck": "tsc --noEmit",
"test:unit": "vitest run"
},
"dependencies": {
"@agentic/platform-core": "workspace:*",
"@agentic/platform-openapi": "workspace:*",
"@agentic/platform-schemas": "workspace:*",
"@agentic/platform-validators": "workspace:*",
"@hono/zod-openapi": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"semver": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/semver": "catalog:",
"restore-cursor": "catalog:",
"zod-to-json-schema": "catalog:"
},
"publishConfig": {
"access": "public"
}
}

Wyświetl plik

@ -1,10 +1,9 @@
import { parseZodSchema } from '@agentic/platform-core'
import {
type AgenticProjectConfig,
type AgenticProjectConfigInput,
agenticProjectConfigSchema
} from './agentic-project-config'
} from '@agentic/platform-schemas'
/**
* This method allows Agentic projects to define their configs in a type-safe

Wyświetl plik

@ -0,0 +1,4 @@
export * from './define-config'
export * from './validate-agentic-project-config'
export * from './validate-origin-adapter'
export * from './validate-tools'

Wyświetl plik

@ -1,17 +1,17 @@
import type { ZodTypeDef } from 'zod'
import { assert, type Logger, parseZodSchema } from '@agentic/platform-core'
import { validators } from '@agentic/platform-validators'
import { clean as cleanSemver, valid as isValidSemver } from 'semver'
import type { PricingPlanLineItem } from './pricing'
import {
type AgenticProjectConfig,
type AgenticProjectConfigInput,
agenticProjectConfigSchema,
getPricingPlansByInterval,
type PricingPlanLineItem,
type ResolvedAgenticProjectConfig,
resolvedAgenticProjectConfigSchema
} from './agentic-project-config'
import { getPricingPlansByInterval } from './utils'
} from '@agentic/platform-schemas'
import { validators } from '@agentic/platform-validators'
import { clean as cleanSemver, valid as isValidSemver } from 'semver'
import { validateOriginAdapter } from './validate-origin-adapter'
export async function validateAgenticProjectConfig(

Wyświetl plik

@ -4,7 +4,10 @@ import type {
Tool
} from '@agentic/platform-schemas'
import { assert, type Logger } from '@agentic/platform-core'
import { validateOpenAPISpec } from '@agentic/platform-openapi'
import {
getToolsFromOpenAPISpec,
validateOpenAPISpec
} from '@agentic/platform-openapi'
import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
/**
@ -57,11 +60,25 @@ export async function validateOriginAdapter({
// TODO: Extract tool definitions from OpenAPI operationIds
const dereferencedOpenAPISpec = await validateOpenAPISpec(
originAdapter.spec,
{
cwd,
dereference: true
}
)
const { tools, toolToOperationMap } = await getToolsFromOpenAPISpec(
dereferencedOpenAPISpec
)
return {
tools,
originAdapter: {
...originAdapter,
// Update the openapi spec with the normalized version
spec: JSON.stringify(openapiSpec)
spec: JSON.stringify(openapiSpec),
toolToOperationMap
}
}
} else if (originAdapter.type === 'mcp') {

Wyświetl plik

@ -0,0 +1,42 @@
import type { OriginAdapter, Tool, ToolConfig } from '@agentic/platform-schemas'
import { assert } from '@agentic/platform-core'
/**
* Validates and normalizes the origin adapter config for a project.
*/
export async function validateTools({
originAdapter,
tools,
toolConfigs,
label
}: {
originAdapter: OriginAdapter
tools: Tool[]
toolConfigs: ToolConfig[]
label: string
}): Promise<void> {
assert(tools.length > 0, 400, `No tools defined for ${label}`)
const toolsMap: Record<string, Tool> = {}
for (const tool of tools) {
assert(
!toolsMap[tool.name],
400,
`Duplicate tool name "${tool.name}" found in ${label}`
)
toolsMap[tool.name] = tool
}
for (const toolConfig of toolConfigs) {
const tool = toolsMap[toolConfig.name]
assert(
tool,
400,
`Tool "${toolConfig.name}" from \`toolConfigs\` not found in \`tools\` for ${label}`
)
}
if (originAdapter.type === 'openapi') {
// TODO
}
}

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@fisch0920/config/tsconfig-node",
"include": ["src", "*.config.ts", "bin/*", "../schemas/src/utils.ts"],
"exclude": ["node_modules"]
}

Wyświetl plik

@ -23,18 +23,12 @@
"test:unit": "vitest run"
},
"dependencies": {
"@agentic/platform-core": "workspace:*",
"@agentic/platform-openapi": "workspace:*",
"@agentic/platform-validators": "workspace:*",
"@hono/zod-openapi": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"ms": "catalog:",
"semver": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/ms": "catalog:",
"@types/semver": "catalog:",
"restore-cursor": "catalog:",
"zod-to-json-schema": "catalog:"
},

Wyświetl plik

@ -1,5 +1,4 @@
export * from './agentic-project-config'
export * from './define-config'
export * from './mcp'
export * from './origin-adapter'
export * from './pricing'
@ -7,6 +6,4 @@ export * from './rate-limit'
export * from './subjects'
export * from './tools'
export * from './utils'
export * from './validate-agentic-project-config'
export * from './validate-origin-adapter'
export * from './webhook'

Wyświetl plik

@ -1,6 +1,7 @@
import { z } from '@hono/zod-openapi'
import { mcpServerInfoSchema } from './mcp'
import { toolNameSchema } from './tools'
export const originAdapterLocationSchema = z.literal('external')
// z.union([
@ -103,6 +104,43 @@ NOTE: Agentic currently only supports \`external\` API servers. If you'd like to
.openapi('OriginAdapterConfig')
export type OriginAdapterConfig = z.infer<typeof originAdapterConfigSchema>
export const openapiOperationParameterSourceSchema = z.union([
z.literal('query'),
z.literal('header'),
z.literal('path'),
z.literal('cookie'),
z.literal('body'),
z.literal('formData')
])
export type OpenAPIOperationParameterSource = z.infer<
typeof openapiOperationParameterSourceSchema
>
export const openapiOperationHttpMethodSchema = z.union([
z.literal('get'),
z.literal('put'),
z.literal('post'),
z.literal('delete'),
z.literal('patch'),
z.literal('trace')
])
export type OpenAPIOperationHttpMethod = z.infer<
typeof openapiOperationHttpMethodSchema
>
export const openapiToolOperationSchema = z.object({
operationId: z.string().describe('OpenAPI operationId for the tool'),
method: openapiOperationHttpMethodSchema.describe('HTTP method'),
path: z.string().describe('HTTP path template'),
parameterSources: z
.record(z.string(), openapiOperationParameterSourceSchema)
.describe(
'Mapping from parameter name to HTTP source (query, path, JSON body, etc).'
),
tags: z.array(z.string()).optional()
})
export type OpenAPIToolOperation = z.infer<typeof openapiToolOperationSchema>
export const openapiOriginAdapterSchema = commonOriginAdapterSchema.merge(
z.object({
/**
@ -121,11 +159,21 @@ export const openapiOriginAdapterSchema = commonOriginAdapterSchema.merge(
.string()
.describe(
'JSON stringified OpenAPI spec describing the origin API server.'
)
),
// TODO: Mapping from tool names to OpenAPI operations, with all the info
// the Agentic API gateway needs to know at runtime (HTTP method, path,
// params, etc).
/**
* Mapping from tool name to OpenAPI Operation info.
*
* This is used by the Agentic API gateway to route tools to the correct
* origin API operation, along with the HTTP method, path, params, etc.
*
* @internal
*/
toolToOperationMap: z
.record(toolNameSchema, openapiToolOperationSchema)
.describe(
'Mapping from tool name to OpenAPI Operation info. This is used by the Agentic API gateway to route tools to the correct origin API operation, along with the HTTP method, path, params, etc.'
)
})
)

Wyświetl plik

@ -1,4 +1,4 @@
import { z } from 'zod'
import { z } from '@hono/zod-openapi'
export const subjectSchemas = {
user: z.object({

Wyświetl plik

@ -134,10 +134,7 @@ export const toolConfigSchema = z
'Map of PricingPlan slug to tool config overrides for a given plan. This is useful to customize tool behavior or disable tools completely on different pricing plans.'
)
// TODO: mapping from OpenAPI operationId to tools
// TODO?
// name
// path, httpMethod
// examples
// headers
})

Wyświetl plik

@ -1,4 +1,8 @@
import type { PricingInterval, PricingPlan, PricingPlanList } from './pricing'
import type {
PricingInterval,
PricingPlan,
PricingPlanList
} from '@agentic/platform-schemas'
export function getPricingPlansByInterval({
pricingInterval,

Wyświetl plik

@ -1,9 +1,11 @@
export type ParsedFaasIdentifier = {
projectIdentifier: string
toolPath: string
deploymentHash?: string
deploymentIdentifier?: string
version?: string
// TODO: Rename to `toolName`?
toolPath: string
} & (
| {
deploymentHash: string

Wyświetl plik

@ -251,6 +251,9 @@ importers:
apps/api:
dependencies:
'@agentic/platform':
specifier: workspace:*
version: link:../../packages/platform
'@agentic/platform-core':
specifier: workspace:*
version: link:../../packages/core
@ -330,6 +333,9 @@ importers:
apps/gateway:
dependencies:
'@agentic/platform':
specifier: workspace:*
version: link:../../packages/platform
'@agentic/platform-api-client':
specifier: workspace:*
version: link:../../packages/api-client
@ -383,6 +389,9 @@ importers:
packages/cli:
dependencies:
'@agentic/platform':
specifier: workspace:*
version: link:../platform
'@agentic/platform-api-client':
specifier: workspace:*
version: link:../api-client
@ -463,18 +472,27 @@ importers:
packages/fixtures:
dependencies:
'@agentic/platform-schemas':
'@agentic/platform':
specifier: workspace:*
version: link:../schemas
version: link:../platform
packages/openapi:
dependencies:
'@agentic/platform-core':
specifier: workspace:*
version: link:../core
'@agentic/platform-schemas':
specifier: workspace:*
version: link:../schemas
'@redocly/openapi-core':
specifier: 'catalog:'
version: 1.34.3(supports-color@10.0.0)
camelcase:
specifier: ^8.0.0
version: 8.0.0
decamelize:
specifier: ^6.0.0
version: 6.0.0
devDependencies:
'@hono/node-server':
specifier: 'catalog:'
@ -483,7 +501,7 @@ importers:
specifier: 'catalog:'
version: 4.7.10
packages/schemas:
packages/platform:
dependencies:
'@agentic/platform-core':
specifier: workspace:*
@ -491,6 +509,9 @@ importers:
'@agentic/platform-openapi':
specifier: workspace:*
version: link:../openapi
'@agentic/platform-schemas':
specifier: workspace:*
version: link:../schemas
'@agentic/platform-validators':
specifier: workspace:*
version: link:../validators
@ -500,9 +521,6 @@ importers:
'@modelcontextprotocol/sdk':
specifier: 'catalog:'
version: 1.12.0
ms:
specifier: 'catalog:'
version: 2.1.3
semver:
specifier: 'catalog:'
version: 7.7.2
@ -510,9 +528,6 @@ importers:
specifier: 'catalog:'
version: 3.25.30
devDependencies:
'@types/ms':
specifier: 'catalog:'
version: 2.1.0
'@types/semver':
specifier: 'catalog:'
version: 7.7.0
@ -523,6 +538,28 @@ importers:
specifier: 'catalog:'
version: 3.24.5(zod@3.25.30)
packages/schemas:
dependencies:
'@hono/zod-openapi':
specifier: 'catalog:'
version: 0.19.6(hono@4.7.10)(zod@3.25.30)
ms:
specifier: 'catalog:'
version: 2.1.3
zod:
specifier: 'catalog:'
version: 3.25.30
devDependencies:
'@types/ms':
specifier: 'catalog:'
version: 2.1.0
restore-cursor:
specifier: 'catalog:'
version: 5.1.0
zod-to-json-schema:
specifier: 'catalog:'
version: 3.24.5(zod@3.25.30)
packages/validators:
dependencies:
'@paralleldrive/cuid2':
@ -2184,6 +2221,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001715:
resolution: {integrity: sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==}
@ -2345,6 +2386,10 @@ packages:
supports-color:
optional: true
decamelize@6.0.0:
resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
decircular@0.1.1:
resolution: {integrity: sha512-V2Vy+QYSXdgxRPmOZKQWCDf1KQNTUP/Eqswv/3W20gz7+6GB1HTosNrWqK3PqstVpFw/Dd/cGTmXSTKPeOiGVg==}
engines: {node: '>=18'}
@ -6135,6 +6180,8 @@ snapshots:
callsites@3.1.0: {}
camelcase@8.0.0: {}
caniuse-lite@1.0.30001715: {}
chai@5.2.0:
@ -6284,6 +6331,8 @@ snapshots:
optionalDependencies:
supports-color: 10.0.0
decamelize@6.0.0: {}
decircular@0.1.1: {}
deep-eql@5.0.2: {}