77 "io"
88 "net/http"
99 "strings"
10+ "time"
1011
1112 ghErrors "github.com/github/github-mcp-server/pkg/errors"
1213 "github.com/github/github-mcp-server/pkg/inventory"
@@ -16,6 +17,7 @@ import (
1617 "github.com/google/go-github/v79/github"
1718 "github.com/google/jsonschema-go/jsonschema"
1819 "github.com/modelcontextprotocol/go-sdk/mcp"
20+ "github.com/shurcooL/githubv4"
1921)
2022
2123const (
@@ -1868,3 +1870,333 @@ func extractPaginationOptionsFromArgs(args map[string]any) (github.ListProjectsP
18681870
18691871 return opts , nil
18701872}
1873+
1874+ func CreateProject (t translations.TranslationHelperFunc ) inventory.ServerTool {
1875+ tool := NewTool (
1876+ ToolsetMetadataProjects ,
1877+ mcp.Tool {
1878+ Name : "create_project" ,
1879+ Description : t ("TOOL_CREATE_PROJECT_DESCRIPTION" , "Create a new GitHub Project (ProjectsV2). Returns the project ID and number." ),
1880+ Annotations : & mcp.ToolAnnotations {
1881+ Title : t ("TOOL_CREATE_PROJECT_USER_TITLE" , "Create project" ),
1882+ ReadOnlyHint : false ,
1883+ },
1884+ InputSchema : & jsonschema.Schema {
1885+ Type : "object" ,
1886+ Properties : map [string ]* jsonschema.Schema {
1887+ "owner" : {
1888+ Type : "string" ,
1889+ Description : "GitHub username or organization name" ,
1890+ },
1891+ "owner_type" : {
1892+ Type : "string" ,
1893+ Description : "Owner type" ,
1894+ Enum : []any {"user" , "org" },
1895+ },
1896+ "title" : {
1897+ Type : "string" ,
1898+ Description : "Project title" ,
1899+ },
1900+ "description" : {
1901+ Type : "string" ,
1902+ Description : "Project description (optional)" ,
1903+ },
1904+ },
1905+ Required : []string {"owner" , "owner_type" , "title" },
1906+ },
1907+ },
1908+ []scopes.Scope {scopes .Project },
1909+ func (ctx context.Context , deps ToolDependencies , _ * mcp.CallToolRequest , args map [string ]any ) (* mcp.CallToolResult , any , error ) {
1910+ owner , err := RequiredParam [string ](args , "owner" )
1911+ if err != nil {
1912+ return utils .NewToolResultError (err .Error ()), nil , nil
1913+ }
1914+ ownerType , err := RequiredParam [string ](args , "owner_type" )
1915+ if err != nil {
1916+ return utils .NewToolResultError (err .Error ()), nil , nil
1917+ }
1918+ title , err := RequiredParam [string ](args , "title" )
1919+ if err != nil {
1920+ return utils .NewToolResultError (err .Error ()), nil , nil
1921+ }
1922+
1923+ gqlClient , err := deps .GetGQLClient (ctx )
1924+ if err != nil {
1925+ return utils .NewToolResultError (err .Error ()), nil , nil
1926+ }
1927+
1928+ ownerId , err := getOwnerNodeID (ctx , gqlClient , owner , ownerType )
1929+ if err != nil {
1930+ return utils .NewToolResultError (fmt .Sprintf ("failed to get owner ID: %v" , err )), nil , nil
1931+ }
1932+
1933+ var mutation struct {
1934+ CreateProjectV2 struct {
1935+ ProjectV2 struct {
1936+ ID string
1937+ Number int
1938+ Title string
1939+ URL string
1940+ }
1941+ } `graphql:"createProjectV2(input: $input)"`
1942+ }
1943+
1944+ input := githubv4.CreateProjectV2Input {
1945+ OwnerID : githubv4 .ID (ownerId ),
1946+ Title : githubv4 .String (title ),
1947+ }
1948+
1949+ err = gqlClient .Mutate (ctx , & mutation , input , nil )
1950+ if err != nil {
1951+ return utils .NewToolResultError (fmt .Sprintf ("failed to create project: %v" , err )), nil , nil
1952+ }
1953+
1954+ return MarshalledTextResult (mutation .CreateProjectV2 .ProjectV2 ), nil , nil
1955+ },
1956+ )
1957+ tool .FeatureFlagEnable = FeatureFlagConsolidatedProjects
1958+ return tool
1959+ }
1960+
1961+ func CreateIterationField (t translations.TranslationHelperFunc ) inventory.ServerTool {
1962+ tool := NewTool (
1963+ ToolsetMetadataProjects ,
1964+ mcp.Tool {
1965+ Name : "create_iteration_field" ,
1966+ Description : t ("TOOL_CREATE_ITERATION_FIELD_DESCRIPTION" , "Create an iteration field on a ProjectsV2 with weekly sprints. Returns field ID and iteration IDs." ),
1967+ Annotations : & mcp.ToolAnnotations {
1968+ Title : t ("TOOL_CREATE_ITERATION_FIELD_USER_TITLE" , "Create iteration field" ),
1969+ ReadOnlyHint : false ,
1970+ },
1971+ InputSchema : & jsonschema.Schema {
1972+ Type : "object" ,
1973+ Properties : map [string ]* jsonschema.Schema {
1974+ "owner" : {
1975+ Type : "string" ,
1976+ Description : "GitHub username or organization name" ,
1977+ },
1978+ "owner_type" : {
1979+ Type : "string" ,
1980+ Description : "Owner type" ,
1981+ Enum : []any {"user" , "org" },
1982+ },
1983+ "project_number" : {
1984+ Type : "number" ,
1985+ Description : "The project's number" ,
1986+ },
1987+ "field_name" : {
1988+ Type : "string" ,
1989+ Description : "Field name (e.g., 'Sprint', 'Iteration')" ,
1990+ },
1991+ "duration" : {
1992+ Type : "number" ,
1993+ Description : "Duration in days for each iteration (typically 7 for weekly)" ,
1994+ },
1995+ "start_date" : {
1996+ Type : "string" ,
1997+ Description : "Start date in YYYY-MM-DD format" ,
1998+ },
1999+ "iterations" : {
2000+ Type : "array" ,
2001+ Description : "Array of iteration definitions" ,
2002+ Items : & jsonschema.Schema {
2003+ Type : "object" ,
2004+ Properties : map [string ]* jsonschema.Schema {
2005+ "title" : {
2006+ Type : "string" ,
2007+ },
2008+ "startDate" : {
2009+ Type : "string" ,
2010+ },
2011+ "duration" : {
2012+ Type : "number" ,
2013+ },
2014+ },
2015+ Required : []string {"title" , "startDate" , "duration" },
2016+ },
2017+ },
2018+ },
2019+ Required : []string {"owner" , "owner_type" , "project_number" , "field_name" , "duration" , "start_date" , "iterations" },
2020+ },
2021+ },
2022+ []scopes.Scope {scopes .Project },
2023+ func (ctx context.Context , deps ToolDependencies , _ * mcp.CallToolRequest , args map [string ]any ) (* mcp.CallToolResult , any , error ) {
2024+ owner , err := RequiredParam [string ](args , "owner" )
2025+ if err != nil {
2026+ return utils .NewToolResultError (err .Error ()), nil , nil
2027+ }
2028+ ownerType , err := RequiredParam [string ](args , "owner_type" )
2029+ if err != nil {
2030+ return utils .NewToolResultError (err .Error ()), nil , nil
2031+ }
2032+ projectNumber , err := RequiredInt (args , "project_number" )
2033+ if err != nil {
2034+ return utils .NewToolResultError (err .Error ()), nil , nil
2035+ }
2036+ fieldName , err := RequiredParam [string ](args , "field_name" )
2037+ if err != nil {
2038+ return utils .NewToolResultError (err .Error ()), nil , nil
2039+ }
2040+ duration , err := RequiredInt (args , "duration" )
2041+ if err != nil {
2042+ return utils .NewToolResultError (err .Error ()), nil , nil
2043+ }
2044+ startDateStr , err := RequiredParam [string ](args , "start_date" )
2045+ if err != nil {
2046+ return utils .NewToolResultError (err .Error ()), nil , nil
2047+ }
2048+ // Handle iterations array
2049+ rawIterations , ok := args ["iterations" ].([]any )
2050+ if ! ok {
2051+ return utils .NewToolResultError ("iterations must be an array" ), nil , nil
2052+ }
2053+
2054+ client , err := deps .GetClient (ctx )
2055+ if err != nil {
2056+ return utils .NewToolResultError (err .Error ()), nil , nil
2057+ }
2058+ gqlClient , err := deps .GetGQLClient (ctx )
2059+ if err != nil {
2060+ return utils .NewToolResultError (err .Error ()), nil , nil
2061+ }
2062+
2063+ projectId , err := getProjectNodeID (ctx , client , owner , ownerType , projectNumber )
2064+ if err != nil {
2065+ return utils .NewToolResultError (fmt .Sprintf ("failed to get project ID: %v" , err )), nil , nil
2066+ }
2067+
2068+ // Step 1: Create Field
2069+ var createMutation struct {
2070+ CreateProjectV2Field struct {
2071+ ProjectV2Field struct {
2072+ ProjectV2IterationField struct {
2073+ ID string
2074+ Name string
2075+ } `graphql:"... on ProjectV2IterationField"`
2076+ }
2077+ } `graphql:"createProjectV2Field(input: $input)"`
2078+ }
2079+
2080+ createInput := githubv4.CreateProjectV2FieldInput {
2081+ ProjectID : githubv4 .ID (projectId ),
2082+ DataType : githubv4 .ProjectV2CustomFieldTypeIteration ,
2083+ Name : githubv4 .String (fieldName ),
2084+ }
2085+
2086+ err = gqlClient .Mutate (ctx , & createMutation , createInput , nil )
2087+ if err != nil {
2088+ return utils .NewToolResultError (fmt .Sprintf ("failed to create iteration field: %v" , err )), nil , nil
2089+ }
2090+
2091+ fieldId := createMutation .CreateProjectV2Field .ProjectV2Field .ProjectV2IterationField .ID
2092+
2093+ // Step 2: Update Field Configuration
2094+ var updateMutation struct {
2095+ UpdateProjectV2Field struct {
2096+ ProjectV2Field struct {
2097+ ProjectV2IterationField struct {
2098+ ID string
2099+ Name string
2100+ Configuration struct {
2101+ Iterations []struct {
2102+ ID string
2103+ Title string
2104+ StartDate string
2105+ Duration int
2106+ }
2107+ }
2108+ } `graphql:"... on ProjectV2IterationField"`
2109+ }
2110+ } `graphql:"updateProjectV2Field(input: $input)"`
2111+ }
2112+
2113+ var iterationsInput []githubv4.ProjectV2IterationFieldIterationInput
2114+ for _ , item := range rawIterations {
2115+ iterMap , ok := item .(map [string ]any )
2116+ if ! ok { continue }
2117+ title , _ := iterMap ["title" ].(string )
2118+ sDate , _ := iterMap ["startDate" ].(string )
2119+ dur , _ := iterMap ["duration" ].(float64 )
2120+
2121+ parsedSDate , err := time .Parse ("2006-01-02" , sDate )
2122+ if err != nil {
2123+ return utils .NewToolResultError (fmt .Sprintf ("failed to parse iteration startDate %s: %v" , sDate , err )), nil , nil
2124+ }
2125+
2126+ iterationsInput = append (iterationsInput , githubv4.ProjectV2IterationFieldIterationInput {
2127+ Title : githubv4 .String (title ),
2128+ StartDate : githubv4.Date {Time : parsedSDate },
2129+ Duration : githubv4 .Int (dur ),
2130+ })
2131+ }
2132+
2133+ parsedStartDate , err := time .Parse ("2006-01-02" , startDateStr )
2134+ if err != nil {
2135+ return utils .NewToolResultError (fmt .Sprintf ("failed to parse start_date %s: %v" , startDateStr , err )), nil , nil
2136+ }
2137+
2138+ configInput := githubv4.ProjectV2IterationFieldConfigurationInput {
2139+ Duration : githubv4 .Int (duration ),
2140+ StartDate : githubv4.Date {Time : parsedStartDate },
2141+ Iterations : & iterationsInput ,
2142+ }
2143+
2144+ updateInput := githubv4.UpdateProjectV2FieldInput {
2145+ ProjectID : githubv4 .ID (projectId ),
2146+ FieldID : githubv4 .ID (fieldId ),
2147+ IterationConfiguration : & configInput ,
2148+ }
2149+
2150+ err = gqlClient .Mutate (ctx , & updateMutation , updateInput , nil )
2151+ if err != nil {
2152+ return utils .NewToolResultError (fmt .Sprintf ("failed to update iteration configuration: %v" , err )), nil , nil
2153+ }
2154+
2155+ return MarshalledTextResult (updateMutation .UpdateProjectV2Field .ProjectV2Field .ProjectV2IterationField ), nil , nil
2156+ },
2157+ )
2158+ tool .FeatureFlagEnable = FeatureFlagConsolidatedProjects
2159+ return tool
2160+ }
2161+
2162+ func getOwnerNodeID (ctx context.Context , client * githubv4.Client , owner string , ownerType string ) (string , error ) {
2163+ if ownerType == "org" {
2164+ var query struct {
2165+ Organization struct {
2166+ ID string
2167+ } `graphql:"organization(login: $login)"`
2168+ }
2169+ variables := map [string ]interface {}{
2170+ "login" : githubv4 .String (owner ),
2171+ }
2172+ err := client .Query (ctx , & query , variables )
2173+ return query .Organization .ID , err
2174+ } else {
2175+ var query struct {
2176+ User struct {
2177+ ID string
2178+ } `graphql:"user(login: $login)"`
2179+ }
2180+ variables := map [string ]interface {}{
2181+ "login" : githubv4 .String (owner ),
2182+ }
2183+ err := client .Query (ctx , & query , variables )
2184+ return query .User .ID , err
2185+ }
2186+ }
2187+
2188+ func getProjectNodeID (ctx context.Context , client * github.Client , owner , ownerType string , projectNumber int ) (string , error ) {
2189+ if ownerType == "org" {
2190+ project , _ , err := client .Projects .GetOrganizationProject (ctx , owner , projectNumber )
2191+ if err != nil {
2192+ return "" , err
2193+ }
2194+ return project .GetNodeID (), nil
2195+ } else {
2196+ project , _ , err := client .Projects .GetUserProject (ctx , owner , projectNumber )
2197+ if err != nil {
2198+ return "" , err
2199+ }
2200+ return project .GetNodeID (), nil
2201+ }
2202+ }
0 commit comments