Skip to main content
Glama
provider.ts9.82 kB
import { ExpandedPropSpecFor } from "../../spec/props.ts"; import { ExpandedPkgSpec } from "../../spec/pkgs.ts"; import pluralize from "npm:pluralize@^8.0.0"; import { CfProperty, PipelineOptions, PropertyNormalizationContext, PROVIDER_REGISTRY, ProviderConfig, ProviderFuncSpecs, ProviderFunctions, SuperSchema, } from "../types.ts"; import { DIGITALOCEAN_PROP_OVERRIDES, DIGITALOCEAN_SCHEMA_OVERRIDES } from "./overrides.ts"; import { makeModule } from "../generic/index.ts"; import { htmlToMarkdown } from "../../util.ts"; import { ACTION_FUNC_SPECS, CODE_GENERATION_FUNC_SPECS, MANAGEMENT_FUNCS, QUALIFICATION_FUNC_SPECS, } from "./funcs.ts"; import { DigitalOceanOpenApiDocument, DigitalOceanSchema, type JsonSchemaObject, type OperationData, } from "./schema.ts"; import { mergeResourceOperations, normalizeDigitalOceanProperty } from "./spec.ts"; import { generateDigitalOceanSpecs } from "./pipeline.ts"; import { JSONSchema } from "../draft_07.ts"; import SwaggerParser from "@apidevtools/swagger-parser"; import logger from "../../logger.ts"; function createDocLink( schema: SuperSchema, defName: string | undefined, propName?: string, ): string { const docLink = "https://docs.digitalocean.com/reference/api/digitalocean/"; // Use the docTag from the schema if available const digitalOceanSchema = schema as DigitalOceanSchema; const tagName = digitalOceanSchema.docTag || schema.typeName.replace("DigitalOcean ", ""); // Replace spaces with hyphens for URL compatibility const tag = tagName.replace(/ /g, '-'); if (defName) { return `${docLink}#tag/${tag}`; } if (propName) { return `${docLink}#tag/${tag}`; } return `${docLink}#tag/${tag}`; } function digitalOceanCategory(_schema: SuperSchema): string { // Return just the provider name for category return "DigitalOcean"; } function digitalOceanIsChildRequired( schema: SuperSchema | DigitalOceanSchema, _parentProp: ExpandedPropSpecFor["object" | "array" | "map"] | undefined, childName: string, ): boolean { if (!("requiredProperties" in schema)) { throw new Error("Expected DigitalOcean schema with requiredProperties Set"); } return schema.requiredProperties.has(childName); } function digitalOceanNormalizeProperty( prop: JSONSchema, _context: PropertyNormalizationContext, ): CfProperty { let propToNormalize = prop; if ( typeof prop === "object" && "properties" in prop && prop.properties && !prop.type ) { propToNormalize = { ...prop, type: "object" } as CfProperty; } return normalizeDigitalOceanProperty( propToNormalize as JsonSchemaObject, ) as CfProperty; } export function digitalOceanParseRawSchema( allSchemas: DigitalOceanOpenApiDocument, ): ExpandedPkgSpec[] { const specs: ExpandedPkgSpec[] = []; const resourceOperations: Record<string, OperationData[]> = {}; Object.entries(allSchemas.paths || {}).forEach( ([endpoint, openApiDescription]) => { if (!openApiDescription) return; const pathParts = [] as string[]; const urlSections = endpoint.split("/"); if (urlSections.length < 3 || urlSections[1] !== 'v2') { logger.warn(`DigitalOcean endpoint ${endpoint} does not match expected format (/v2/*). Skipping.`); return; } let isSubAsset = false; let hasId = false; for (const urlSection of urlSections.slice(2)) { // If we got an id and are still moving down, this is a sub-asset that should be skipped. if (hasId) { isSubAsset = true; break; } // If id section, we don't add it to the pathParts, but we use it if (urlSection.startsWith("{")) { hasId = true; continue; } pathParts.push(urlSection); } if (isSubAsset) return; // Skipping endpoints that do not represent assets if (pathParts[0] === "add-ons") return; if (pathParts[0] === "gen-ai") return; const methods = `${ openApiDescription.get !== undefined ? "GET " : "" }${ openApiDescription.post !== undefined ? "POST " : "" }${ openApiDescription.put !== undefined ? "PUT " : "" }${ openApiDescription.patch !== undefined ? "PATCH " : "" }${ openApiDescription.delete !== undefined ? "DELETE" : "" }` logger.debug(`${endpoint} -> ${methods}`); // Generate a name for the resource based on the path parts // Make them singular and capitalized before joining const acronymList = ['nfs', 'vpc', 'cdn', 'api', 'ssh', 'ip', 'ipv6', 'uuid', 'byoip']; const acronyms = new Set(acronymList); // Since tech words are often weird, we set a list of words that don't lose the final S when singular. acronymList.forEach(pluralize.addUncountableRule); pluralize.addUncountableRule('kubernetes'); const schemaNameRaw = pathParts // Split on underscores and hyphens to handle compound words .flatMap(part => part.split(/[-_]/)) .map(word => { // Singularize the word const singular = pluralize.singular(word); if (acronyms.has(singular.toLowerCase())) { return singular.toUpperCase(); } // Otherwise capitalize the first letter return singular.charAt(0).toUpperCase() + singular.slice(1); }) .join(' '); // For historical reasons we need to rename these assets to match the names we already had const nameOverrides = { "App": "App Platform", "Registry": "Container Registry", "Image": "Custom Image", "Database": "Database Cluster", "Monitoring Alert": "Monitoring Alert Policy", "Account Key": "SSH Key", "Certificate": "SSL Certificate", } as Record<string, string>; const schemaName = nameOverrides[schemaNameRaw] || schemaNameRaw; // DO has /v2/registry and v2/registries, and we only use one of them since the operations overlap. if ( schemaName === "Container Registry" && endpoint === "/v2/registry" ) { return; } resourceOperations[schemaName] ??= []; resourceOperations[schemaName].push({ endpoint, endpointHasId: hasId, openApiDescription: { get: openApiDescription.get, post: openApiDescription.post, put: openApiDescription.put, patch: openApiDescription.patch, delete: openApiDescription.delete, }, }); }, ); Object.entries(resourceOperations).forEach(([schemaName, operations]) => { const hasPost = operations.some(op => op.openApiDescription.post !== undefined); const hasGet = operations.some(op => op.openApiDescription.get !== undefined); if (operations.length > 2) { logger.warn(`DigitalOcean ${schemaName} has more than 2 operations. Skipping.`); return; } const endpointWithId = operations .find(op => op.endpointHasId); const endpointWithoutId = operations .find(op => !op.endpointHasId); if (!hasPost || !hasGet || !endpointWithId || !endpointWithoutId) return; // Extract description from tag const firstOp = operations[0]?.openApiDescription; const tags = firstOp?.get?.tags || firstOp?.post?.tags; const tagName = tags?.[0]; let description: string | undefined; if (tagName && allSchemas.tags) { const tag = allSchemas.tags.find((t) => t.name === tagName); if (tag?.description) { description = tag.description; } } const result = mergeResourceOperations( schemaName, endpointWithoutId.endpoint, operations, description, ); if (!result) return; const spec = makeModule( result.schema, htmlToMarkdown(result.schema.description) ?? result.schema.description, result.onlyProperties, digitalOceanProviderConfig, result.domainProperties, result.resourceValueProperties, ); specs.push(spec); }); logger.info(`Parsed ${specs.length} DigitalOcean schemas.`) return specs; } async function digitalOceanLoadSchemas(options: PipelineOptions) { return await generateDigitalOceanSpecs(options); } async function digitalOceanFetchSchema() { const url = "https://api-engineering.nyc3.cdn.digitaloceanspaces.com/spec-ci/DigitalOcean-public.v2.yaml"; console.log(`Fetching DigitalOcean OpenAPI spec from ${url}...`); // Use SwaggerParser to fetch and parse (but not dereference) the spec const spec = await SwaggerParser.parse(url); await Deno.writeTextFile( "./src/provider-schemas/digitalocean.json", JSON.stringify(spec, null, 2), ); } const digitalOceanProviderFunctions: ProviderFunctions = { createDocLink, getCategory: digitalOceanCategory, }; const digitalOceanProviderFuncSpecs: ProviderFuncSpecs = { actions: ACTION_FUNC_SPECS, codeGeneration: CODE_GENERATION_FUNC_SPECS, management: MANAGEMENT_FUNCS, qualification: QUALIFICATION_FUNC_SPECS, }; export const digitalOceanProviderConfig: ProviderConfig = { name: "digitalocean", isStable: true, functions: digitalOceanProviderFunctions, funcSpecs: digitalOceanProviderFuncSpecs, loadSchemas: digitalOceanLoadSchemas, fetchSchema: digitalOceanFetchSchema, metadata: { color: "#0080FF", displayName: "DigitalOcean", description: "DigitalOcean cloud infrastructure", }, normalizeProperty: digitalOceanNormalizeProperty, isChildRequired: digitalOceanIsChildRequired, overrides: { propOverrides: DIGITALOCEAN_PROP_OVERRIDES, schemaOverrides: DIGITALOCEAN_SCHEMA_OVERRIDES, }, }; // Register this provider PROVIDER_REGISTRY[digitalOceanProviderConfig.name] = digitalOceanProviderConfig;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server