Repository: enzonotario/vitepress-openapi (ts)

Issue:
Overwrite "Try It Out" url
### What would you like?

The ability to change the default url/port for the "Try It Out" button. 

### Why is this needed?

The API I'm building has different ports for different things. In my specific case, the current behavior does not invoke my api. 

### How could it be implemented?

Could we possibly  have an additional env variable that overwrites the default behavior?

### Other information

_No response_

Pull request:
feat(servers): default/custom/multiple
<!-- Provide a general summary of your changes in the title above. -->

# Description

- Allows to set a default base url
- Allows to set a custom function to get servers for an operation
- Parses `servers` at operation level
- In playground, allows to select Server from multiple if has more than one

## Related issues/external references

- Closes #127 

## Types of changes

- New feature 


Patch:
diff --git a/dev/schemas/index.md b/dev/schemas/index.md
index ed1f299..da4687e 100644
--- a/dev/schemas/index.md
+++ b/dev/schemas/index.md
@@ -5,10 +5,38 @@ title: vitepress-openapi
 ---
 
 <script setup lang="ts">
+import { onUnmounted } from 'vue'
 import { useData } from 'vitepress'
+import { useTheme } from 'vitepress-openapi'
 import spec from '../../docs/public/openapi-schemas.json'
 
 const { isDark } = useData()
+
+useTheme({
+  operation: {
+    defaultBaseUrl: 'https://default.base.url',
+    getServers: ({method, path, operation}) => {
+      if (operation.operationId === 'getCircularReference') {
+        return [] // To force the default base URL.
+      }
+
+      // Generate between 1 and 3 random servers.
+      const randomServers = Math.floor(Math.random() * 3) + 1
+
+      return Array.from({length: randomServers}, (_, index) => {
+        const randomPort = Math.floor(Math.random() * 1000) + 3000
+        return {
+          url: `https://${method.toLowerCase()}.${operation.operationId}.local:${randomPort}`,
+          description: `Localhost ${index + 1}`,
+        }
+      })
+    },
+  },
+})
+
+onUnmounted(() => {
+  useTheme().reset()
+})
 </script>
 
 <OASpec :spec="spec" :isDark="isDark" />
diff --git a/docs/composables/useTheme.md b/docs/composables/useTheme.md
index 8d44d2e..0fe6d69 100644
--- a/docs/composables/useTheme.md
+++ b/docs/composables/useTheme.md
@@ -67,6 +67,10 @@ export default {
                 hiddenSlots: [],
                 // Set the number of columns to use in the OAOperation component.
                 cols: 2,
+                // Set the default base URL.
+                defaultBaseUrl: 'http://localhost',
+                // Set a custom function to get servers for an operation.
+                getServers: ({ method, path, operation }) => string[],
             },
             // Set the i18n configuration.
             i18n: {
@@ -145,9 +149,10 @@ export default {
 
 ## Operation Configuration
 
-| Function             | Description                                        | Default Value    | Allowed Values                  |
-|----------------------|----------------------------------------------------|------------------|---------------------------------|
-| `setOperationBadges` | Sets the operation badges. The order is respected. | `['deprecated']` | `['deprecated', 'operationId']` |
+| Function                     | Description                                        | Default Value        | Allowed Values                  |
+|------------------------------|----------------------------------------------------|----------------------|---------------------------------|
+| `setOperationBadges`         | Sets the operation badges. The order is respected. | `['deprecated']`     | `['deprecated', 'operationId']` |
+| `setOperationDefaultBaseUrl` | Sets the default base URL.                         | `'http://localhost'` | `string`                        |
 
 ## I18n Configuration
 
diff --git a/docs/example/schemas/index.md b/docs/example/schemas/index.md
index 6930247..90594b4 100644
--- a/docs/example/schemas/index.md
+++ b/docs/example/schemas/index.md
@@ -5,10 +5,38 @@ title: vitepress-openapi
 ---
 
 <script setup lang="ts">
+import { onUnmounted } from 'vue'
 import { useData } from 'vitepress'
+import { useTheme } from 'vitepress-openapi'
 import spec from '../../public/openapi-schemas.json'
 
 const { isDark } = useData()
+
+useTheme({
+  operation: {
+    defaultBaseUrl: 'https://default.base.url',
+    getServers: ({method, path, operation}) => {
+      if (operation.operationId === 'getCircularReference') {
+        return [] // To force the default base URL.
+      }
+
+      // Generate between 1 and 3 random servers.
+      const randomServers = Math.floor(Math.random() * 3) + 1
+
+      return Array.from({length: randomServers}, (_, index) => {
+        const randomPort = Math.floor(Math.random() * 1000) + 3000
+        return {
+          url: `https://${method.toLowerCase()}.${operation.operationId}.local:${randomPort}`,
+          description: `Localhost ${index + 1}`,
+        }
+      })
+    },
+  },
+})
+
+onUnmounted(() => {
+  useTheme().reset()
+})
 </script>
 
 <OASpec :spec="spec" :isDark="isDark" />
diff --git a/docs/public/openapi.json b/docs/public/openapi.json
index 221c629..2ef4824 100644
--- a/docs/public/openapi.json
+++ b/docs/public/openapi.json
@@ -80,7 +80,15 @@
               }
             }
           }
-        }
+        },
+        "servers": [
+            {
+                "url": "https://localhost:3000"
+            },
+            {
+                "url": "https://stoplight.io/mocks/enzonotario/argentine-rock/122547792"
+            }
+        ]
       },
       "post": {
         "tags": [
diff --git a/src/components/Common/VPSwitch.vue b/src/components/Common/VPSwitch.vue
index 36af1cb..3241200 100644
--- a/src/components/Common/VPSwitch.vue
+++ b/src/components/Common/VPSwitch.vue
@@ -1,7 +1,7 @@
 <template>
   <button class="VPSwitch" type="button" role="switch">
     <span class="check">
-      <span class="icon" v-if="$slots.default">
+      <span v-if="$slots.default" class="icon">
         <slot />
       </span>
     </span>
diff --git a/src/components/Operation/OAOperation.vue b/src/components/Operation/OAOperation.vue
index f6e044b..85099f3 100644
--- a/src/components/Operation/OAOperation.vue
+++ b/src/components/Operation/OAOperation.vue
@@ -264,6 +264,8 @@ function hasSlot(name: OperationSlot): boolean {
         :base-url="path.baseUrl"
         :hide-base-url="path.hideBaseUrl"
         :deprecated="path.deprecated"
+        :servers="path.servers"
+        @update:selected-server="path.updateSelectedServer"
       />
     </template>
 
@@ -292,6 +294,7 @@ function hasSlot(name: OperationSlot): boolean {
           :security-schemes="tryIt.securitySchemes"
           :content-type="tryIt.contentType"
           :is-dark="isDark"
+          @update:selected-server="tryIt.updateSelectedServer"
           @update:request="tryIt.updateRequest"
         />
       </ClientOnly>
diff --git a/src/components/Path/OAPath.vue b/src/components/Path/OAPath.vue
index 44b65f0..52025ff 100644
--- a/src/components/Path/OAPath.vue
+++ b/src/components/Path/OAPath.vue
@@ -1,5 +1,6 @@
 <script setup>
 import { computed, defineProps, ref } from 'vue'
+import { useStorage } from '@vueuse/core'
 import { getOpenApiInstance } from '../../lib/getOpenApiInstance'
 import { buildRequest } from '../../lib/codeSamples/buildRequest'
 import { useTheme } from '../../composables/useTheme'
@@ -25,8 +26,6 @@ const operationPath = openapi.getOperationPath(props.id)
 
 const operationMethod = openapi.getOperationMethod(props.id)?.toUpperCase()
 
-const baseUrl = openapi.getBaseUrl(props.id)
-
 const securitySchemes = openapi.getSecuritySchemes(props.id)
 
 const operationParsed = openapi.getParsedOperation(props.id)
@@ -45,6 +44,36 @@ const bodyRequestContentTypes = computed(() => operationRequestBody ? Object.key
 
 const bodyRequestContentType = computed(() => bodyRequestContentTypes.value.length ? bodyRequestContentTypes.value[0] : undefined)
 
+const customGetServers = themeConfig.getOperationServers()
+
+const servers = customGetServers
+  ? customGetServers({
+    method: operationMethod,
+    path: operationPath,
+    operation,
+  })
+  : openapi.getOperationServers(props.id)
+
+const defaultServer = servers.length ? servers[0]?.url : themeConfig.getOperationDefaultBaseUrl()
+
+const selectedServer = servers?.length > 1
+  ? useStorage(`--oa-operation-${props.id}-selectedServer`, defaultServer, localStorage, {
+    mergeDefaults: true,
+  })
+  : ref(defaultServer)
+
+const baseUrl = computed(() => {
+  const value = selectedServer.value
+
+  if (servers.length > 1 && !servers.some(server => server.url === value)) {
+    updateSelectedServer(servers[0]?.url)
+
+    return servers[0]?.url
+  }
+
+  return value
+})
+
 const shouldBuildRequest = computed(() => ['try-it', 'code-samples'].some(slot => operationSlots.value.includes(slot)))
 
 const request = ref(
@@ -52,7 +81,7 @@ const request = ref(
     ? buildRequest({
       path: operationPath,
       method: operationMethod,
-      baseUrl,
+      baseUrl: baseUrl.value,
       parameters: operationParameters ?? [],
       authScheme: securitySchemes.length ? securitySchemes[0] : undefined,
       body: operationRequestBody?.content?.[bodyRequestContentType]?.uiContentType,
@@ -64,6 +93,10 @@ const request = ref(
 function updateRequest(newRequest) {
   request.value = newRequest
 }
+
+function updateSelectedServer(server) {
+  selectedServer.value = server
+}
 </script>
 
 <template>
@@ -118,6 +151,8 @@ function updateRequest(newRequest) {
                 :path="operationPath"
                 :hide-base-url="!themeConfig.getShowBaseURL()"
                 :deprecated="operation.deprecated"
+                :servers="servers"
+                :update-selected-server="updateSelectedServer"
               />
             </div>
 
@@ -195,6 +230,8 @@ function updateRequest(newRequest) {
                 :path="operationPath"
                 :hide-base-url="!themeConfig.getShowBaseURL()"
                 :deprecated="operation.deprecated"
+                :servers="servers"
+                :update-selected-server="updateSelectedServer"
               />
             </div>
 
@@ -211,6 +248,8 @@ function updateRequest(newRequest) {
                 :content-type="bodyRequestContentType"
                 :request="request"
                 :update-request="updateRequest"
+                :servers="servers"
+                :update-selected-server="updateSelectedServer"
               />
             </template>
 
diff --git a/src/components/Path/OAPathEndpoint.vue b/src/components/Path/OAPathEndpoint.vue
index ec46ab1..fbe07c0 100644
--- a/src/components/Path/OAPathEndpoint.vue
+++ b/src/components/Path/OAPathEndpoint.vue
@@ -1,4 +1,5 @@
 <script setup>
+import { defineEmits } from 'vue'
 import OAMethodBadge from '../Common/OAMethodBadge.vue'
 
 const props = defineProps({
@@ -22,25 +23,58 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  servers: {
+    type: Array,
+    required: false,
+  },
 })
+
+const emits = defineEmits([
+  'update:selectedServer',
+])
 </script>
 
 <template>
-  <div class="flex flex-row items-center space-x-4 text-sm bg-muted rounded p-2 overflow-x-auto">
-    <OAMethodBadge :method="props.method" />
-    <span class="flex flex-row flex-shrink-0 text-gray-600 dark:text-gray-400">
-      <span
-        v-if="!props.hideBaseUrl"
-        class="hidden md:inline-block"
-      >
-        {{ props.baseUrl }}
+  <div class="flex flex-col gap-2 text-sm bg-muted rounded p-2">
+    <div class="flex flex-row items-center gap-4 overflow-x-auto">
+      <OAMethodBadge :method="props.method" />
+      <span class="flex flex-row flex-shrink-0 text-gray-600 dark:text-gray-400">
+        <span
+          v-if="!props.hideBaseUrl"
+          class="hidden md:inline-block"
+        >
+          {{ props.baseUrl }}
+        </span>
+        <span
+          :class="{
+            'line-through': props.deprecated,
+          }"
+          class="text-gray-800 dark:text-gray-200 font-bold select-all"
+        >{{ props.path }}</span>
       </span>
-      <span
-        :class="{
-          'line-through': props.deprecated,
-        }"
-        class="text-gray-800 dark:text-gray-200 font-bold select-all"
-      >{{ props.path }}</span>
-    </span>
+    </div>
+
+    <div
+      v-if="props.servers && props.servers.length > 1"
+      class="flex flex-row items-center gap-2"
+    >
+      <span class="text-gray-600 dark:text-gray-400">
+        {{ $t('Server') }}
+      </span>
+      <select
+        :value="props.baseUrl"
+        class="w-full rounded p-1 text-sm bg-white dark:bg-gray-800 dark:text-gray-200"
+        @change="($event) => emits('update:selectedServer', $event.target.value)"
+      >
+        <option
+          v-for="(server, idx) in props.servers"
+          :key="`${server.url}-${idx}`"
+          :value="server.url"
+          :selected="server.url === props.baseUrl"
+        >
+          {{ server.url }}
+        </option>
+      </select>
+    </div>
   </div>
 </template>
diff --git a/src/components/Playground/OAPlaygroundParameters.vue b/src/components/Playground/OAPlaygroundParameters.vue
index e1460e7..e4135f4 100644
--- a/src/components/Playground/OAPlaygroundParameters.vue
+++ b/src/components/Playground/OAPlaygroundParameters.vue
@@ -114,7 +114,7 @@ function setAuthScheme(scheme: PlaygroundSecurityScheme) {
   }
 }
 
-watch([variables, authScheme, body], () => {
+watch([variables, authScheme, body, () => props.baseUrl], () => {
   emits('update:request', buildRequest({
     baseUrl: props.baseUrl,
     method: props.method as OpenAPIV3.HttpMethods,
diff --git a/src/components/Try/OATryWithVariables.vue b/src/components/Try/OATryWithVariables.vue
index 008a749..6bc716f 100644
--- a/src/components/Try/OATryWithVariables.vue
+++ b/src/components/Try/OATryWithVariables.vue
@@ -55,6 +55,7 @@ const props = defineProps({
 
 const emits = defineEmits([
   'update:request',
+  'update:selectedServer',
 ])
 
 const loading = ref(false)
@@ -72,7 +73,8 @@ const loading = ref(false)
       :security-schemes="props.securitySchemes ?? {}"
       :schema-ui-content-type="props.requestBody?.content?.[props.contentType]?.uiContentType"
       :is-dark="props.isDark"
-      @update:request="($event) => emits('update:request', $event)"
+      @update:request="($event: any) => emits('update:request', $event)"
+      @update:selected-server="($event: any) => emits('update:selectedServer', $event)"
     />
 
     <OATryItButton
diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts
index 964ea44..4811f90 100644
--- a/src/composables/useTheme.ts
+++ b/src/composables/useTheme.ts
@@ -1,12 +1,12 @@
+import type { Ref } from 'vue'
 import { ref } from 'vue'
 import vitesseLight from 'shiki/themes/vitesse-light.mjs'
 import vitesseDark from 'shiki/themes/vitesse-dark.mjs'
-import type { Ref } from 'vue'
 import type { IOARequest } from '../lib/codeSamples/request'
 import { generateCodeSample } from '../lib/codeSamples/generateCodeSample'
 import { deepUnref } from '../lib/deepUnref'
 import { locales } from '../locales'
-import type { OperationSlot } from '../types'
+import type { OperationSlot, ParsedOperation } from '../types'
 
 export interface ThemeConfig {
   highlighterTheme: {
@@ -67,6 +67,8 @@ export interface OperationConfig {
   slots?: Ref<OperationSlot[]>
   hiddenSlots?: Ref<OperationSlot[]>
   cols?: Ref<1 | 2>
+  defaultBaseUrl?: string
+  getServers?: GetServersFunction | null
 }
 
 export type Languages = 'es' | 'en' | 'pt-BR' | string
@@ -144,10 +146,12 @@ export interface UseThemeConfigUnref {
     selectedScheme: string | null
   }>
   operation?: Partial<{
-    badges: OperationBadges[]
-    slots: OperationSlot[]
-    hiddenSlots: OperationSlot[]
-    cols: 1 | 2
+    badges?: OperationBadges[]
+    slots?: OperationSlot[]
+    hiddenSlots?: OperationSlot[]
+    cols?: 1 | 2
+    defaultBaseUrl?: string
+    getServers?: null
   }>
   i18n?: Partial<I18nConfig>
   spec?: Partial<SpecConfig>
@@ -176,6 +180,8 @@ interface LanguageConfig {
 
 type GeneratorFunction = (lang: string, request: IOARequest) => string
 
+type GetServersFunction = ({ method, path, operation }: { method: string, path: string, operation: ParsedOperation }) => string[] | null
+
 export const DEFAULT_OPERATION_SLOTS: OperationSlot[] = [
   'header',
   'path',
@@ -190,6 +196,8 @@ export const DEFAULT_OPERATION_SLOTS: OperationSlot[] = [
   'footer',
 ]
 
+export const DEFAULT_BASE_URL = 'http://localhost'
+
 const availableLanguages: LanguageConfig[] = [
   {
     lang: 'curl',
@@ -260,6 +268,8 @@ const themeConfig: UseThemeConfig = {
     slots: ref(DEFAULT_OPERATION_SLOTS),
     hiddenSlots: ref([]),
     cols: ref(2),
+    defaultBaseUrl: DEFAULT_BASE_URL,
+    getServers: null,
   },
   i18n: {
     locale: ref<Languages>('en'),
@@ -388,6 +398,15 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
       setOperationCols(config.operation.cols)
     }
 
+    if (config?.operation?.defaultBaseUrl !== undefined) {
+      setOperationDefaultBaseUrl(config.operation.defaultBaseUrl)
+    }
+
+    if (config?.operation?.getServers !== undefined) {
+      // @ts-expect-error: This is a valid expression.
+      themeConfig.operation.getServers = config.operation.getServers
+    }
+
     if (config?.i18n !== undefined) {
       setI18nConfig(config.i18n)
     }
@@ -599,6 +618,19 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
     themeConfig.operation.cols.value = value
   }
 
+  function getOperationDefaultBaseUrl(): string {
+    return themeConfig?.operation?.defaultBaseUrl || DEFAULT_BASE_URL
+  }
+
+  function setOperationDefaultBaseUrl(value: string) {
+    // @ts-expect-error: This is a valid expression.
+    themeConfig.operation.defaultBaseUrl = value
+  }
+
+  function getOperationServers(): GetServersFunction | null {
+    return themeConfig?.operation?.getServers || null
+  }
+
   function getI18nConfig(): I18nConfig {
     return themeConfig.i18n as I18nConfig
   }
@@ -785,6 +817,8 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
     setOperationHiddenSlots,
     getOperationCols,
     setOperationCols,
+    getOperationDefaultBaseUrl,
+    getOperationServers,
     getI18nConfig,
     setI18nConfig,
     getSpecConfig,
diff --git a/src/lib/OpenApi.ts b/src/lib/OpenApi.ts
index f086c7f..005b48a 100644
--- a/src/lib/OpenApi.ts
+++ b/src/lib/OpenApi.ts
@@ -3,8 +3,6 @@ import { httpVerbs } from '../index'
 import type { ParsedOpenAPI } from '../types'
 import { processOpenAPI } from './processOpenAPI'
 
-const DEFAULT_SERVER_URL = 'http://localhost'
-
 export function OpenApi({
   spec,
   parsedSpec,
@@ -41,51 +39,6 @@ export function OpenApi({
     return parsedSpec
   }
 
-  function getBaseUrl(operationId?: string) {
-    if (operationId) {
-      const operationPath = getOperationPath(operationId)
-      if (operationPath) {
-        const pathServers = getSpec().paths[operationPath]?.servers
-        if (pathServers && pathServers.length > 0) {
-          try {
-            const firstUrl = pathServers[0].url
-
-            if (!firstUrl) {
-              throw new Error('Invalid URL')
-            }
-
-            const isValid = new URL(firstUrl)
-            if (!isValid) {
-              throw new Error('Invalid URL')
-            }
-
-            return firstUrl
-          } catch {
-            console.warn('Invalid server URL in path servers:', pathServers)
-          }
-        }
-      }
-    }
-
-    if (!getSpec().servers || getSpec().servers.length === 0) {
-      return DEFAULT_SERVER_URL
-    }
-
-    try {
-      const firstUrl = getSpec().servers[0].url
-
-      const isValid = new URL(firstUrl)
-      if (!isValid) {
-        throw new Error('Invalid URL')
-      }
-
-      return firstUrl
-    } catch {
-      console.warn('Invalid server URL:', getSpec().servers)
-      return DEFAULT_SERVER_URL
-    }
-  }
-
   function getOperation(operationId: string) {
     if (!getSpec().paths) {
       return null
@@ -207,6 +160,24 @@ export function OpenApi({
     return getSpec().servers ?? []
   }
 
+  function getOperationServers(operationId: string) {
+    const operation = findOperation(getPaths(), operationId)
+
+    if (!operation) {
+      return []
+    }
+
+    const operationPath = getOperationPath(operationId)
+
+    const pathServers = getSpec().paths[(operationPath ?? '')]?.servers
+
+    return [
+      ...(operation?.servers ?? []),
+      ...(pathServers ?? []),
+      ...(getSpec().servers ?? []),
+    ]
+  }
+
   function getOperationsTags(): string[] {
     if (!getSpec().paths) {
       return []
@@ -286,7 +257,6 @@ export function OpenApi({
     spec: getSpec(),
     transformedSpec,
     parsedSpec,
-    getBaseUrl,
     getOperation,
     getOperationPath,
     getOperationMethod,
@@ -299,6 +269,7 @@ export function OpenApi({
     getInfo,
     getExternalDocs,
     getServers,
+    getOperationServers,
     getOperationsTags,
     getPathsByTags,
     getPathsWithoutTags,
diff --git a/src/locales/en.json b/src/locales/en.json
index b2b900b..b17474c 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -30,6 +30,7 @@
   "API Documentation": "API Documentation",
   "External Documentation": "External Documentation",
   "Servers": "Servers",
+  "Server": "Server",
   "Your browser does not support the audio element.": "Your browser does not support the audio element.",
   "Unrecognized response type. Raw content:": "Unrecognized response type. Raw content:",
   "Download file": "Download file",
diff --git a/src/locales/es.json b/src/locales/es.json
index ac6e6eb..b096fb4 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -30,6 +30,7 @@
   "API Documentation": "Documentación de la API",
   "External Documentation": "Documentación externa",
   "Servers": "Servidores",
+  "Server": "Servidor",
   "Your browser does not support the audio element.": "Tu navegador no soporta el elemento de audio.",
   "Unrecognized response type. Raw content:": "Tipo de respuesta no reconocido. Contenido en crudo:",
   "Download file": "Descargar archivo",
diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json
index 889ddb2..3e5c45d 100644
--- a/src/locales/pt-BR.json
+++ b/src/locales/pt-BR.json
@@ -30,6 +30,7 @@
   "API Documentation": "Documentação da API",
   "External Documentation": "Documentação Externa",
   "Servers": "Servidores",
+  "Server": "Servidor",
   "Your browser does not support the audio element.": "Seu navegador não suporta o elemento de áudio.",
   "Unrecognized response type. Raw content:": "Tipo de resposta não reconhecido. Conteúdo bruto:",
   "Download file": "Baixar arquivo",


Test patch:
diff --git a/test/composables/openapi.test.ts b/test/composables/openapi.test.ts
index 1258af8..6b6894b 100644
--- a/test/composables/openapi.test.ts
+++ b/test/composables/openapi.test.ts
@@ -21,11 +21,6 @@ describe('openapi with spec', () => {
     expect(result).toEqual(spec.paths['/users'].get.parameters)
   })
 
-  it('returns the correct base URL for getBaseUrl', () => {
-    const result = openapi.getBaseUrl()
-    expect(result).toBe('https://api.example.com')
-  })
-
   it('returns the correct tags for getTags', () => {
     const result = openapi.getOperationsTags()
     expect(result).toEqual(['users', 'pets'])
@@ -195,6 +190,14 @@ describe('openapi with spec', () => {
     const tags = api.getTags()
     expect(tags).toEqual([])
   })
+
+  it('returns the correct operation servers for getOperationServers', () => {
+    const result = openapi.getOperationServers('getUsers')
+    expect(result).toEqual([
+      ...spec.paths['/users'].get.servers,
+      ...spec.servers,
+    ])
+  })
 })
 
 describe('spec with different servers for specific path', () => {
@@ -211,34 +214,58 @@ describe('spec with different servers for specific path', () => {
           operationId: 'useGlobalServer',
         },
       },
-      '/use-local-server': {
+      '/use-path-server': {
         get: {
-          operationId: 'useLocalServer',
+          operationId: 'usePathServer',
         },
         servers: [
           {
-            url: 'https://api.local.com',
+            url: 'https://api.path.com',
           },
         ],
       },
+      '/use-local-server': {
+        get: {
+          operationId: 'useLocalServer',
+          servers: [
+            {
+              url: 'https://api.local.com',
+            },
+          ],
+        },
+      },
     },
   }
 
   const openapi = createOpenApiInstance({ spec })
 
-  it('returns global server for getBaseUrl', () => {
-    const result = openapi.getBaseUrl()
-    expect(result).toBe('https://api.example.com')
-  })
+  it('returns correct servers for getOperationServers', () => {
+    const useGlobalServer = openapi.getOperationServers('useGlobalServer')
+    expect(useGlobalServer).toEqual([
+      {
+        url: 'https://api.example.com',
+      },
+    ])
 
-  it('returns global server for useGlobalServer', () => {
-    const result = openapi.getBaseUrl('useGlobalServer')
-    expect(result).toBe('https://api.example.com')
-  })
+    const usePathServer = openapi.getOperationServers('usePathServer')
+    expect(usePathServer).toEqual([
+      {
+        url: 'https://api.path.com',
+      },
+      {
+        url: 'https://api.example.com',
+      },
+    ])
 
-  it('returns local server for useLocalServer', () => {
-    const result = openapi.getBaseUrl('useLocalServer')
-    expect(result).toBe('https://api.local.com')
+    const useLocalServer = openapi.getOperationServers('useLocalServer')
+    expect(useLocalServer).toEqual([
+      {
+        url: 'https://api.local.com',
+      },
+      {
+        url: 'https://api.example.com',
+      },
+    ])
   })
 })
 
diff --git a/test/composables/useOpenapiWithNoSpec.test.ts b/test/composables/useOpenapiWithNoSpec.test.ts
index 9bbaf3a..c16067b 100644
--- a/test/composables/useOpenapiWithNoSpec.test.ts
+++ b/test/composables/useOpenapiWithNoSpec.test.ts
@@ -19,9 +19,14 @@ describe('useOpenapi with no spec', () => {
     expect(result).toEqual([])
   })
 
-  it('returns empty string for getBaseUrl when servers are not defined', () => {
-    const result = openapi.getBaseUrl()
-    expect(result).toBe('http://localhost')
+  it('returns empty array for getOperationServers when servers are not defined', () => {
+    const result = openapi.getOperationServers('someOperationId')
+    expect(result).toEqual([])
+  })
+
+  it('returns empty array for getServers when servers are not defined', () => {
+    const result = openapi.getServers()
+    expect(result).toEqual([])
   })
 
   it('returns empty array for getTags when paths are not defined', () => {
diff --git a/test/composables/useTheme.test.ts b/test/composables/useTheme.test.ts
index a0fd82a..800d07c 100644
--- a/test/composables/useTheme.test.ts
+++ b/test/composables/useTheme.test.ts
@@ -1,5 +1,5 @@
 import { beforeEach, describe, expect, it } from 'vitest'
-import { useTheme } from '../../src/composables/useTheme'
+import { DEFAULT_BASE_URL, useTheme } from '../../src/composables/useTheme'
 
 describe('composition API', () => {
   it('can config', () => {
@@ -33,6 +33,8 @@ describe('composition API', () => {
       },
       operation: {
         badges: ['operationId'],
+        defaultBaseUrl: 'https://app.local',
+        getServers: ({ method, path, operation }) => (`https://app.local/${method}${path}`),
       },
       response: {
         maxTabs: 10,
@@ -78,6 +80,8 @@ describe('composition API', () => {
       h6: 6,
     })
     expect(theme.getOperationBadges()).toEqual(['operationId'])
+    expect(theme.getOperationDefaultBaseUrl()).toBe('https://app.local')
+    expect(theme.getOperationServers()).toBeTypeOf('function')
     expect(theme.getResponseCodeMaxTabs()).toBe(10)
     expect(theme.getResponseCodeSelector()).toBe('select')
     expect(theme.getSecurityDefaultScheme()).toBe('bearer')
@@ -108,6 +112,8 @@ describe('composition API', () => {
       },
       operation: {
         badges: ['operationId'],
+        defaultBaseUrl: 'https://app.local',
+        getServers: ({ method, path, operation }) => (`https://app.local/${method}${path}`),
       },
       i18n: {
         locale: 'es',
@@ -125,6 +131,8 @@ describe('composition API', () => {
       h6: 6,
     })
     expect(theme.getOperationBadges()).toEqual(['operationId'])
+    expect(theme.getOperationDefaultBaseUrl()).toBe('https://app.local')
+    expect(theme.getOperationServers()).toBeTypeOf('function')
     expect(theme.getI18nConfig().locale.value).toBe('es')
 
     theme.reset()
@@ -140,6 +148,8 @@ describe('composition API', () => {
       h6: 6,
     })
     expect(theme.getOperationBadges()).toEqual(['deprecated'])
+    expect(theme.getOperationDefaultBaseUrl()).toBe(DEFAULT_BASE_URL)
+    expect(theme.getOperationServers()).toBe(null)
     expect(theme.getI18nConfig().locale.value).toBe('en')
   })
 })
diff --git a/test/lib/__snapshots__/processOpenAPI.test.ts.snap b/test/lib/__snapshots__/processOpenAPI.test.ts.snap
index bda3d2d..5fe0f35 100644
--- a/test/lib/__snapshots__/processOpenAPI.test.ts.snap
+++ b/test/lib/__snapshots__/processOpenAPI.test.ts.snap
@@ -741,6 +741,14 @@ All descriptions *can* contain ~~tons of text~~ **Markdown**. [If GitHub support
         "security": [
           {},
         ],
+        "servers": [
+          {
+            "url": "https://localhost:3000",
+          },
+          {
+            "url": "https://stoplight.io/mocks/enzonotario/argentine-rock/122547792",
+          },
+        ],
         "summary": "Get all artists",
         "tags": [
           "Artists",
diff --git a/test/testsConstants.ts b/test/testsConstants.ts
index 315f6a1..a06d336 100644
--- a/test/testsConstants.ts
+++ b/test/testsConstants.ts
@@ -39,6 +39,14 @@ export const spec = {
         'tags': [
           'users',
         ],
+        'servers': [
+          {
+            url: 'http://localhost:3000',
+          },
+          {
+            url: 'https://api.example.com',
+          },
+        ],
       },
     },
     '/users/{id}': {