-
Notifications
You must be signed in to change notification settings - Fork 3
Project variables tool #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
91aa162
WIP
andrewabest b269cb7
Adds function to correctly check if variables are in git
andrewabest 67a2969
Cleanup
andrewabest c799099
Ensure tool registration occurs
andrewabest 3bdc762
Correct types and urls
andrewabest 082dad2
await function call
andrewabest 1c6ba23
Simplify variable tool payload
andrewabest 7bdbb03
Remove tenant variables, cleanup types
andrewabest b484f58
Cleans up tool description
andrewabest 134d2c6
Review tidy up
andrewabest 30e3924
Address feedback
andrewabest File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
import { | ||
Client, | ||
type Project, | ||
ProjectRepository, | ||
resolveSpaceId, type ResourcesById | ||
} from "@octopusdeploy/api-client"; | ||
import { z } from "zod"; | ||
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
import { getClientConfigurationFromEnvironment } from "../helpers/getClientConfigurationFromEnvironment.js"; | ||
import { registerToolDefinition } from "../types/toolConfig.js"; | ||
import type {ResourceCollection} from "@octopusdeploy/api-client/dist/resourceCollection.js"; | ||
|
||
export function registerGetVariablesTool(server: McpServer) { | ||
server.tool( | ||
"get_variables", | ||
`This tool gets all project and library variable set variables for a given project. | ||
Projects can contain variables (specific to a project), library variable sets (shared collections of variables associated with many projects), | ||
and tenant variables (variables related to a tenants connected to the project) | ||
If you want to retrieve tenant variables for a tenant connected to the project, use the get_tenant_variables tool. | ||
`, | ||
{ | ||
spaceName: z.string().describe("The space name"), | ||
projectId: z.string().describe("The ID of the project to retrieve the variables for"), | ||
gitRef: z.string().describe("The gitRef to retrieve the variables from, if the project is a config-as-code project").optional(), | ||
}, | ||
{ | ||
title: "Get variables for a Project from Octopus Deploy", | ||
readOnlyHint: true, | ||
}, | ||
async ({ spaceName, projectId, gitRef }) => { | ||
|
||
const configuration = getClientConfigurationFromEnvironment(); | ||
const client = await Client.create(configuration); | ||
const spaceId = await resolveSpaceId(client, spaceName); | ||
|
||
const variables = await getAllVariables({ | ||
projectId: projectId, | ||
spaceName: spaceName, | ||
spaceId: spaceId, | ||
gitRef: gitRef | ||
}, client) | ||
|
||
return { | ||
content: [ | ||
{ | ||
type: "text", | ||
text: JSON.stringify({ | ||
variables | ||
}), | ||
}, | ||
], | ||
}; | ||
} | ||
); | ||
} | ||
|
||
registerToolDefinition({ | ||
toolName: "get_variables", | ||
config: { toolset: "projects", readOnly: true }, | ||
registerFn: registerGetVariablesTool, | ||
}); | ||
|
||
type VariableResource = { | ||
Id: string; | ||
Name: string; | ||
Value: string | null; | ||
Description: string | undefined; | ||
Scope: ScopeSpecification; | ||
IsEditable: boolean; | ||
Prompt: Readonly<unknown>; | ||
Type: unknown; | ||
IsSensitive: boolean; //false; // For backwards compatibility | ||
} | ||
|
||
type Arrays<T> = { | ||
[P in keyof T]: Array<T[P]>; | ||
}; | ||
|
||
type ScopeSpecification = Arrays<ScopeSpecificationTypes>; | ||
|
||
interface ScopeSpecificationTypes { | ||
Environment?: string; | ||
Machine?: string; | ||
Role?: string; | ||
Action?: string; | ||
Channel?: string; | ||
TenantTag?: string; | ||
ProcessOwner?: string; | ||
} | ||
|
||
interface VariableSetResource { | ||
Id: string; | ||
SpaceId: string; | ||
OwnerId: string; | ||
ScopeValues: unknown; | ||
Variables: VariableResource[]; | ||
Version: number; | ||
} | ||
|
||
type PropertyValueResource = string | SensitiveValue | null; | ||
|
||
interface SensitiveValue { | ||
HasValue: boolean; | ||
// NewValue can also be null at runtime | ||
NewValue?: string; | ||
Hint?: string; | ||
} | ||
|
||
interface ActionTemplateParameterResource { | ||
Id: string; | ||
Name: string; | ||
Label: string; | ||
HelpText: string; | ||
DefaultValue?: PropertyValueResource; | ||
DisplaySettings: unknown; | ||
AllowClear?: boolean; | ||
} | ||
|
||
interface LibraryVariableSetResource { | ||
Name: string; | ||
SpaceId: string; | ||
Description: string; | ||
VariableSetId: string; | ||
ContentType: unknown; | ||
Templates: ActionTemplateParameterResource[]; | ||
} | ||
|
||
interface LibraryVariableSetWithVariables { | ||
variableSet: VariableSetResponse; | ||
libraryVariableSet: LibraryVariableSetResource; | ||
} | ||
|
||
interface AllVariablesForProject { | ||
projectVariableSet: Omit<VariableSetResource, "ScopeValues"> | undefined; | ||
libraryVariableSets: LibraryVariableSetWithVariables[]; | ||
} | ||
|
||
interface GetAllVariablesParams { | ||
projectId: string; | ||
gitRef?: string; | ||
spaceName: string; | ||
spaceId: string; | ||
} | ||
|
||
type VariableSetResponse = Omit<VariableSetResource, "ScopeValues">; | ||
|
||
export async function getAllVariables( | ||
params: GetAllVariablesParams, | ||
apiClient: Client | ||
): Promise<AllVariablesForProject> { | ||
|
||
const { spaceId, spaceName, gitRef, projectId } = params; | ||
|
||
const projectRepository = new ProjectRepository(apiClient, spaceName); | ||
const project = await projectRepository.get(projectId); | ||
const projectVariableSet = await loadProjectVariableSet(project, gitRef, apiClient, spaceId); | ||
const libraryVariableSets = await loadLibraryVariableSetVariables(project.IncludedLibraryVariableSetIds, apiClient, spaceId); | ||
|
||
return { | ||
projectVariableSet, | ||
libraryVariableSets | ||
}; | ||
} | ||
|
||
async function loadProjectVariableSet( | ||
project: Project, | ||
gitRef: string | undefined, | ||
apiClient: Client, | ||
spaceId: string | ||
): Promise<VariableSetResponse | undefined> { | ||
|
||
// This is a bit hacky, but gets around the limitations of our ts client types without having to define | ||
// a heap of new types. | ||
// We are expecting the type to match { ConversionState: { VariablesAreInGit: true } } | ||
// If the variables are stored in git. | ||
function hasVariablesInGit(value: unknown): boolean { | ||
if (typeof value !== 'object' || value === null || !('ConversionState' in value)) { | ||
return false; | ||
} | ||
|
||
const obj = value as Record<string, unknown>; | ||
const conversionState = obj.ConversionState; | ||
|
||
return ( | ||
typeof conversionState === 'object' && | ||
conversionState !== null && | ||
'VariablesAreInGit' in conversionState && | ||
(conversionState as Record<string, unknown>).VariablesAreInGit === true | ||
); | ||
} | ||
|
||
// Check if project has git persistence | ||
const hasGitVariables = hasVariablesInGit(project.PersistenceSettings); | ||
|
||
if (hasGitVariables && !gitRef) { | ||
throw new Error(`Missing gitRef for config-as-code project ${project.Name}`); | ||
} | ||
|
||
let resource: VariableSetResource; | ||
|
||
if (hasGitVariables) { | ||
// For git projects, we need to get both text and sensitive variables separately | ||
|
||
// Retrieve the variable set stored in git for the associated gitRef | ||
const textVariableSet = await apiClient.get<VariableSetResource>( | ||
`~/api/spaces/${spaceId}/projects/${project.Id}/${gitRef}/variables` | ||
); | ||
|
||
// Sensitive variables are still stored in the database so that they can be encrypted | ||
const sensitiveVariableSet = await apiClient.get<VariableSetResource>( | ||
`~/api/spaces/${spaceId}/projects/${project.Id}/variables` | ||
); | ||
|
||
// Combine variables from both sets | ||
resource = { | ||
...textVariableSet, | ||
Variables: [...textVariableSet.Variables, ...sensitiveVariableSet.Variables] | ||
}; | ||
} else { | ||
// For database projects, get variables directly | ||
resource = await apiClient.get<VariableSetResource>(`~/api/spaces/${spaceId}/variables/${project.VariableSetId}`); | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const { ScopeValues, ...result } = resource; | ||
|
||
return result; | ||
} | ||
|
||
async function loadLibraryVariableSetVariables( | ||
includedLibraryVariableSetIds: string[], | ||
apiClient: Client, | ||
spaceId: string | ||
): Promise<LibraryVariableSetWithVariables[]> { | ||
|
||
if (includedLibraryVariableSetIds.length === 0) return []; | ||
|
||
// Get library variable sets | ||
const libraryVariableSets = await apiClient.get<ResourceCollection<LibraryVariableSetResource>>( | ||
`~/api/spaces/${spaceId}/libraryvariablesets?ids=${includedLibraryVariableSetIds.join(',')}` | ||
); | ||
|
||
// Get all variable sets for the library variable sets | ||
const variableSetIds = libraryVariableSets.Items.map(lvs => lvs.VariableSetId); | ||
const allVariableSets = await apiClient.get<VariableSetResource[]>( | ||
`~/api/spaces/${spaceId}/variables/all?ids=${variableSetIds.join(',')}` | ||
); | ||
|
||
const responseVariableSets: VariableSetResponse[] = allVariableSets.map(resource => { | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const { ScopeValues, ...result } = resource; | ||
return result; | ||
}) | ||
// Create lookup map | ||
const allVariableSetsMap = responseVariableSets.reduce((acc: ResourcesById<VariableSetResponse>, resource) => { | ||
acc[resource.Id] = resource; | ||
return acc; | ||
}, {}); | ||
|
||
// Combine library variable sets with their variable sets | ||
return libraryVariableSets.Items.map(lvs => ({ | ||
variableSet: allVariableSetsMap[lvs.VariableSetId], | ||
libraryVariableSet: lvs | ||
})); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add an explanation for the model as to what variable sets and library variable sets are and their relationship with each other or is it already well understood from the response structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the model really needs to know about that, unless we want it to be able to modify them afterwards. Its more about evaluating the variables available to a project.