Page History
...
| Code Block | ||
|---|---|---|
| ||
[McpServerToolType]
public static class NoteTool
{
[McpServerTool, Description("웹노리 노트에 노트를 추가합니다.")]
public static async Task<string> AddNote(ActorService actorService,
[Description("노트의 제목으로 필수값입니다.")] string title,
[Description("노트의 컨텐츠값으로 필수값입니다.")] string content,
[Description("노트의 카테고리입니다.")] string? category,
[Description("노트에 위치정보가 있다면 latitude를 입력")] double? latitude,
[Description("노트에 위치정보가 있다면 longitude 입력")] double? longitude,
[Description("노트에 임베딩된 값이 있다면 입력")] float[]? tagsEmbeddedAsSingle
)
{
var note = new NoteDocument
{
Title = title,
Category = category,
Content = content,
Latitude = latitude,
Longitude = longitude,
CreatedAt = DateTime.UtcNow,
TagsEmbeddedAsSingle = new RavenVector<float>(tagsEmbeddedAsSingle)
};
actorService.RecordActor.Tell(new AddNoteCommand()
{
Title = title,
Category = category,
Content = content,
Latitude = latitude,
Longitude = longitude,
TagsEmbeddedAsSingle = new RavenVector<float>(tagsEmbeddedAsSingle)
}, ActorRefs.NoSender);
return JsonSerializer.Serialize(note);
}
[McpServerTool, Description("웹노리 노트에서 Text검색을 합니다. 최소 하나값이 필수입니다.")]
public static async Task<string> SearchNoteByText(ActorService actorService,
[Description("웹노리 노트에서 타이틀을 키워드 검색합니다.")] string? title,
[Description("웹노리 노트에서 컨텐츠를 키워드 검색합니다.")] string? content,
[Description("웹노리 노트에서 카테고리를 키워드 검색합니다.")] string? category)
{
var result = await actorService.SearchActor.Ask(new SearchNoteByTextCommand()
{
Title = title,
Content = content,
Category = category
}, TimeSpan.FromSeconds(5));
if (result is SearchNoteActorResult searchResult)
{
return JsonSerializer.Serialize(searchResult.Notes);
}
return "Failed to get note history.";
}
[McpServerTool, Description("웹노리 노트에서 반경검색을 합니다. 모두 필수값입니다.")]
public static async Task<string> SearchNoteByRadius(ActorService actorService,
[Description("This is the latitude of the note ")] double latitude,
[Description("This is the longitude of the note ")] double longitude,
[Description("This is the radius(m) of the note ")] double radius)
{
var result = await actorService.SearchActor.Ask(new SearchNoteByRadiusActorCommand()
{
Latitude = latitude,
Longitude = longitude,
Radius = radius
}, TimeSpan.FromSeconds(5));
if (result is SearchNoteActorResult searchResult)
{
return JsonSerializer.Serialize(searchResult.Notes);
}
return "Failed to get note history.";
}
[McpServerTool, Description("웹노리 노트에서 벡터검색을 합니다. 모두 필수값입니다.")]
public static async Task<string> SearchNoteByVector(ActorService actorService,
[Description("This is the vector of the note If there is no value, enter null.")] float[] vector,
[Description("This is the top N of the note If there is no value, enter null.")] int topN)
{
var result = await actorService.SearchActor.Ask(new SearchNoteByVectorCommand()
{
Vector = vector,
TopN = topN
}, TimeSpan.FromSeconds(5));
if (result is SearchNoteActorResult searchResult)
{
return JsonSerializer.Serialize(searchResult.Notes);
}
return "Failed to get note history.";
}
[McpServerTool, Description("웹노리 노트에서 최근 추가된 노트를 가져옵니다.")]
public static async Task<string> GetNoteHistory(ActorService actorService)
{
//SearchNoteActorResult
var result = await actorService.HistoryActor.Ask(new GetNoteHistoryCommand(), TimeSpan.FromSeconds(5));
if (result is SearchNoteActorResult searchResult)
{
return JsonSerializer.Serialize(searchResult.Notes);
}
return "Failed to get note history.";
}
[McpServerTool, Description("웹노리 노트에서 최근 검색된 노트를 가져옵니다.")]
public static async Task<string> GetNoteSearchHistory(ActorService actorService)
{
//SearchNoteActorResult
var result = await actorService.HistoryActor.Ask(new GetNoteSearchHistoryCommand(), TimeSpan.FromSeconds(5));
if (result is SearchNoteActorResult searchResult)
{
return JsonSerializer.Serialize(searchResult.Notes);
}
return "Failed to get note history.";
}
} |
- Agent LLM이 툴을 선택할때 필요로하는 Description과 구현체를 정리하며 MCP Server에서 사실상 핵심코드영역입니다.
- 작동코드는 모두 액터객체를 이용한 이벤트 방식을 사용합니다.
- Tell : 응답값을 바로 알필요가 없을이 이벤트 전송시사용
- Ask : 이벤트 전송후 해당 이벤트에 반응하는 응답값을 대기할때 사용
- Local Actor를 이용할수도 있지만 복제구성을 해 Client-Server 작동으로 구분하였으며 여기서는 별도로 구동된 Remote에 위치한 서버의 액터를 이용합니다.
- 초기개발은 단일Local StandAlone으로만 작동가능하며 이후 전략에따라 실제 작동액터는 별도 구성된 서버로 코드변경없이 분리할수 있는것이 액터모델의 장점입니다.
클라이언트 서버모델
| Code Block | ||
|---|---|---|
| ||
public class ActorService
{
private readonly ActorSystem actorSystem;
public IActorRef SearchActor { get; set; }
public IActorRef RecordActor { get; set; }
public IActorRef HistoryActor { get; set; }
public ActorService(bool serverMode)
{
Console.WriteLine($"ActorService initialized in {(serverMode ? "Server" : "Client")} mode.");
// HOCON 설정
var config = ConfigurationFactory.ParseString($@"
akka {{
actor {{
provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
}}
remote {{
dot-netty.tcp {{
hostname = ""127.0.0.1""
port = {(serverMode ? 5500 : 0)} // 서버 모드일 때만 포트 5500 사용
}}
}}
}}
");
actorSystem = ActorSystem.Create("MyActorSystem", config);
if (serverMode)
{
// 서버 모드일 때만 작동액터 생성 : MCP Server
SearchActor = actorSystem.ActorOf<SearchActor>("search-actor");
RecordActor = actorSystem.ActorOf<RecordActor>("record-actor");
HistoryActor = actorSystem.ActorOf<HistoryActor>("history-actor");
RecordActor.Tell(new SetHistoryActorCommand()
{
HistoryActor = HistoryActor
});
SearchActor.Tell(new SetHistoryActorCommand()
{
HistoryActor = HistoryActor
});
}
else
{
// 클라이언트 모드일 때 원격 액터 참조 : MCP Client
var remoteAddress = "akka.tcp://MyActorSystem@127.0.0.1:5500";
SearchActor = actorSystem.ActorSelection($"{remoteAddress}/user/search-actor")
.ResolveOne(TimeSpan.FromSeconds(3)).Result;
RecordActor = actorSystem.ActorSelection($"{remoteAddress}/user/record-actor")
.ResolveOne(TimeSpan.FromSeconds(3)).Result;
HistoryActor = actorSystem.ActorSelection($"{remoteAddress}/user/history-actor")
.ResolveOne(TimeSpan.FromSeconds(3)).Result;
}
}
} |
...